講解了Java類和對象在內(nèi)存中的表示機(jī)制,Java對象是根據(jù)Java類創(chuàng)建的,表示一個Java類實(shí)例;Java類是根據(jù)Class文件創(chuàng)建的,Class文件被類加載器加載、鏈接、初始化后就變成Hotspot中的Klass。注意這里的Class文件是指符合Class文件格式規(guī)范的字節(jié)流,并不特指磁盤文件形式的以.class結(jié)尾的class文件,如類加載器也可讀取網(wǎng)絡(luò)字節(jié)流形式的Class文件。Class文件格式是JVM自己定義的用于表示Java類的二進(jìn)制字節(jié)流規(guī)范,與操作系統(tǒng)本身無關(guān),該文件格式正是Java代碼一次編譯,跨平臺運(yùn)行的關(guān)鍵,其他的與底層操作系統(tǒng)強(qiáng)耦合的編譯執(zhí)行的語言如C/C++需要在每個操作系統(tǒng)上都編譯一遍才能正常執(zhí)行,因?yàn)椴煌僮飨到y(tǒng)基本都有特定的匯編器和鏈接器,支持的二進(jìn)制文件格式也可能不同。那么類的字段,方法,繼承的父類,實(shí)現(xiàn)的接口等這些信息在Class文件中是如何組織和表示的了?方法對應(yīng)的字節(jié)碼是是如何運(yùn)行的?
class文件
1、整體結(jié)構(gòu)
每個class文件都有一個ClassFile結(jié)構(gòu),稱為Class文件格式,如下圖:

其中u<n> 表示n個無符號字節(jié),如u4 magic 表示magic的取值用4個無符號字節(jié)表示;cp_info描述常量池的結(jié)構(gòu),field_info描述字段的數(shù)據(jù)結(jié)構(gòu),method_info描述方法的數(shù)據(jù)結(jié)構(gòu),attribute_info描述屬性的數(shù)據(jù)結(jié)構(gòu)。ClassFile結(jié)構(gòu)各項(xiàng)的含義如下:
magic: 魔數(shù),用于標(biāo)識當(dāng)前Class文件的文件格式,JVM可據(jù)此判斷該文件是否可以被解析,目前固定為0xCAFEBABE
minor_version, major_version:minor_version是副版本號,major_version是主版本號,這兩個版本是生成Class文件時根據(jù)編譯的JDK版本來確定的,用標(biāo)識編譯時的JDK版本,常見的一個異常Unsupported major.minor version 52.0就是因?yàn)檫\(yùn)行時的JDK版本低于編譯時的JDK版本,52是Java8的主版本號。
constant_pool_count:常量池計數(shù)器,等于常量池中的成員數(shù)加1。
constant_pool:常量池,是一種表結(jié)構(gòu),包含class文件結(jié)構(gòu)和子結(jié)構(gòu)中引用的所有字符串常量,類或者接口名,字段名和其他常量,其有效索引范圍是1- (constant_pool_count-1)。其中類和接口名采用全限定形式,即在整個JVM中的絕對名稱,如java.lang.Object,方法名,字段名、局部變量名和形參名都采用非限定名,即在源代碼文件中使用相對名稱,如屬性名name。
access_flags:用于表示某個類或者接口的訪問權(quán)限和屬性
this_class:類索引,該值必須是對常量池中某個常量的一個有效索引值,該索引處的成員必須是一個CONSTANT_Class_info類型的結(jié)構(gòu)體,表示這個class文件所定義的類和接口
super_class:父類索引,同this_class,該值必須是對常量池中CONSTANT_Class_info類型常量的一個有效索引值,如果該值為0,則只能表示java.lang.Object類,因?yàn)樵擃愂俏ㄒ灰粋€沒有父類的類。
interfaces_count:接口計數(shù)器,表示當(dāng)前類或者接口的直接超接口的數(shù)量
interfaces:接口表,是一個表結(jié)構(gòu),每個成員同this_class,必須是對常量池中CONSTANT_Class_info類型常量的一個有效索引值,其有效索引范圍為0~interfaces_count,接口表中成員的順序與源代碼中給定的接口順序是一致的,interfaces[0]表示源代碼中最左邊的接口。
fields_count:字段計數(shù)器,當(dāng)前class文件所有字段的數(shù)量
fields:字段表,是一個表結(jié)構(gòu),表中每個成員必須是filed_info數(shù)據(jù)結(jié)構(gòu),用于表示當(dāng)前類或者接口的某個字段的完整描述,不包含從父類或者父接口繼承的字段
methods_count:方法計數(shù)器,表示當(dāng)前類方法表的成員個數(shù)
methods:方法表,是一個表結(jié)構(gòu),表中每個成員必須是method_info數(shù)據(jù)結(jié)構(gòu),用于表示當(dāng)前類或者接口的某個方法的完整描述,包含當(dāng)前類或者接口定義的所有方法,如實(shí)例方法、類方法、實(shí)例初始化方法等,不包含從父類或者父接口繼承的方法
attributes_count:屬性計數(shù)器,表示當(dāng)前class文件attributes屬性表的成員個數(shù)
attributes:屬性表,是一個表結(jié)構(gòu),表中每個成員必須是attribute_info數(shù)據(jù)結(jié)構(gòu),這里的屬性是對class文件本身,方法或者字段的補(bǔ)充描述,如SourceFile屬性用于表示class文件的源代碼文件名。
2、描述符
描述符有兩種,字段描述符和方法描述符,本質(zhì)就是一個基于特定規(guī)則的字符串,其中字段描述符用來表示類,實(shí)例和局部變量的類型,具體如下:

如I表示一個int變量,Ljava.lang.Object;表示一個Object實(shí)例,[[I表示一個二維int數(shù)組實(shí)例。方法描述符包含一個或者多個參數(shù)描述符合一個返回值描述符,參數(shù)描述符和返回值描述符都是上面的字段描述符,再加一個特殊的V,表示該方法不返回任何值。如方法Object m(int i, double d, Thread t) {...}對應(yīng)的方法描述符就是(IDLjava/lang/Thread;)Ljava/lang/Object;
3、cp_info
Java虛擬機(jī)指令不依賴類,接口,類實(shí)例或數(shù)組的運(yùn)行時內(nèi)存布局,而是依賴依賴常量池表中的符號信息,常量池表中所有項(xiàng)都有如下通用格式:

其中tag作為類型標(biāo)記,用于確定后面的info的格式,tag是一個字節(jié),info是兩個或者多個字節(jié),取決于tag的值,如下圖:

以CONSTANT_Utf8_info,CONSTANT_Class_info 和CONSTANT_Fieldref_info 的結(jié)構(gòu)為例說明,如下圖:

CONSTANT_Utf8_info用于表示一個Utf8編碼的字符串,tag取值1,length是后面的byte數(shù)組的長度,byte數(shù)組就是字符串對應(yīng)的byte數(shù)組數(shù)據(jù)了;
CONSTANT_Class_info用于表示一個Java類或者接口名,的tag取值就是上表中的7,name_index表示對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結(jié)構(gòu);
CONSTANT_Fieldref_info用于描述一個字段,其中class_index是常量池中的有效索引,該索引處的成員必須是一個CONSTANT_Class_info結(jié)構(gòu),表示該字段所屬的類,name_and_type_index是常量池中的有效索引,該索引處的成員必須是一個CONSTANT_NameAndType_info結(jié)構(gòu),該結(jié)構(gòu)用于表示一個字段或者方法描述符。
比較特殊的是最后三個,CONSTANT_MethodHandle_info用于表示方法句柄,CONSTANT_MethodType_info用于記錄方法的類型信息,即方法描述符,CONSTANT_InvokeDynamic_info用于表示invokedynamic指令使用的動態(tài)調(diào)用名,參數(shù)和返回值等系列靜態(tài)參數(shù)的常量。
4、filed_info
字段表的成員用field_info結(jié)構(gòu)表示,該結(jié)構(gòu)如下圖:

其中access_flags表示字段的訪問權(quán)限和屬性,是個由標(biāo)識構(gòu)成的掩碼,所謂標(biāo)識就是某個特定的二進(jìn)制位,取值為1表示開啟,0表示關(guān)閉,各標(biāo)識開啟后的含義如下:

以public static final為例說明,public對應(yīng)的標(biāo)識位0000 0000 0000 0001,static對應(yīng)的標(biāo)識位是0000 0000 0000 0100,final對應(yīng)的標(biāo)識位是0000 0000 0001 0000,合起來就是0000 0000 0001 0101。
name_index表示常量池中一個類型為CONSTANT_Utf8_info的有效索引,表示該字段的字段名
descriptor_index 表示常量池中一個類型為CONSTANT_Utf8_info的有效索引,表示該字段的字段描述符
attributes_count 表示當(dāng)前字段的附加屬性表的屬性數(shù)量
attributes 屬性表,表示該字段的所有附加屬性,每個成員都是attribute_info結(jié)構(gòu)
5、method_info
所有的方法都用method_info結(jié)構(gòu)表示,如下圖:

access_flags表示方法的訪問權(quán)限和基本屬性,同filed_info,也是一個由標(biāo)識構(gòu)成的掩碼,各標(biāo)識開啟的含義如下:

其他各字段同filed_info,name_index表示方法名,descriptor_index表示方法描述符,attributes為該方法的屬性表。
6、attribute_info
ClassFile、filed_info、method_info結(jié)構(gòu)和Code屬性都有屬性表,所有的屬性都通過attribute_info結(jié)構(gòu)表示,其通用格式如下:

其中attribute_name_index是常量池中一個類型為CONSTANT_Utf8_info的有效索引,表示該屬性的屬性名,attribute_length表示后面的info信息的字節(jié)長度,這個長度不包括attribute_name_index和attribute_length的6字節(jié)。Java8預(yù)定義了23種屬性,用戶在編譯源代碼文件時可以添加新的屬性,只要JVM實(shí)現(xiàn)能夠正確識別該屬性即可,注意用戶自定義的屬性不能使用這些預(yù)定義屬性的屬性名,各屬性的作用如下:
ConstantValue:位于filed_info的屬性表中,表示static字段的初始值,非對象類型的常量值。
Code:位于method_info的屬性表中,表示該方法的虛擬機(jī)指令及輔助信息,method_info中有且僅有一個Code屬性,其結(jié)構(gòu)如下:

其中max_stack表示當(dāng)前方法操作數(shù)棧的最大深度;max_locals表示此方法引用局部變量表中的局部變量的個數(shù),包含傳遞方法入?yún)⒌木植孔兞?;code_length表示后面的code數(shù)組的字節(jié)長度;code數(shù)組表示當(dāng)前方法的虛擬機(jī)指令的數(shù)據(jù);exception_table_length表示后面的exception_table數(shù)組的長度;exception_table中表示此方法的捕獲的各異常的異常處理邏輯,每個成員對應(yīng)一個異常類型,每個成員包含4個屬性,start_pc, end_pc表示try/catch的代碼范圍,具體來說是起止代碼對應(yīng)的虛擬機(jī)指令在code數(shù)組中的索引,handler_pc是異常處理邏輯的代碼的虛擬機(jī)指令在code數(shù)組中的索引,catch_type是常量池中一個類型為CONSTANT_Class_info的有效索引,表示捕獲的異常類型。
StackMapTable:位于Code屬性的屬性表中,最多只能包含一個,用于虛擬機(jī)的類型檢查驗(yàn)證階段,驗(yàn)證某個局部變量的類型與操作數(shù)棧頂所需的核查類型是否一致。
Exceptions:位于method_info的屬性表,表示該方法可能拋出的受檢異常的異常類型。
InnerClasses: 位于ClassFile的屬性表中,表示該類定義的內(nèi)部類信息,如果有內(nèi)部類,則有且僅有一個InnerClasses屬性
EnclosingMethod :位于ClassFile的屬性表中,如果當(dāng)前類是局部類或者匿名類時才有EnclosingMethod屬性,表示該類的閉包方法。
Synthetic:位于ClassFile,method_info或者filed_info結(jié)構(gòu)的屬性表中,表示該成員沒有在源文件中出現(xiàn),如編譯器自動添加的默認(rèn)構(gòu)造方法。
Signature:位于ClassFile,method_info或者filed_info結(jié)構(gòu)的屬性表中,表示該成員使用的參數(shù)化類型的簽名信息
SourceFile: 位于ClassFile的屬性表中,表示該class文件對應(yīng)的源代碼文件的文件名
SourceDebugExtension:位于ClassFile的屬性表中,表示該類的擴(kuò)展調(diào)試信息
LineNumberTable:位于Code的屬性表中,表示虛擬機(jī)指令同源文件代碼行的對應(yīng)關(guān)系,注意LineNumberTable與源文件的代碼行沒有一一對應(yīng)關(guān)系,可能多個LineNumberTable屬性對應(yīng)同一個代碼行,且LineNumberTable的屬性順序是任意的。
LocalVariableTable:位于Code的屬性表中,表示方法的局部變量,每個局部變量最多對應(yīng)一個LocalVariableTable屬性,Code中的多個LocalVariableTable屬性的順序是任意的。每個局部變量通過5個屬性表示,start_pc和length表示該局部變量的作用域范圍,start_pc是Code數(shù)組的索引,name_index屬性表示局部變量的變量名,descriptor_index表示該變量的字段描述符,index表示該變量在局部變量表中的索引
LocalVariableTypeTable:位于Code的屬性表中,只針對參數(shù)化類型的變量,用于提供該變量參數(shù)化類型的簽名信息,這類變量會同時出現(xiàn)在LocalVariableTable和LocalVariableTypeTable中,其他的變量只在LocalVariableTable中出現(xiàn)。
Deprecated:位于ClassFile,method_info或者filed_info結(jié)構(gòu)的屬性表中,表示此成員在未來版本中被取代
RuntimeVisibleAnnotations:位于ClassFile,method_info或者filed_info結(jié)構(gòu)的屬性表中,最多只能含有一個,表示加在此成員聲明上面的運(yùn)行時可見的注解,注解用annotation結(jié)構(gòu)表示,保存了注解的多個鍵值對屬性。
RuntimeInvisibleAnnotations:與RuntimeVisibleAnnotations相對,表示加在此成員聲明上面的運(yùn)行時不可見的注解
RuntimeVisibleParameterAnnotations:位于method_info結(jié)構(gòu)的屬性表中,最多只能含有一個,表示方法入?yún)⒌倪\(yùn)行時可見的注解
RuntimeInvisibleParameterAnnotations:與RuntimeVisibleParameterAnnotations相對,表示方法入?yún)⒌倪\(yùn)行時不可見的注解
RuntimeVisibleTypeAnnotations:位于ClassFile,method_info、filed_info或者Code結(jié)構(gòu)的屬性表中,記錄了標(biāo)注在對應(yīng)類聲明,字段聲明或者方法聲明所使用的類型上面的運(yùn)行時可見注解,如某個類implements的各個接口的所有注解都會記錄在該類ClassFile結(jié)構(gòu)中的RuntimeVisibleTypeAnnotations屬性中,某個字段的字段類型的所有注解都會記錄在該字段對應(yīng)的filed_info結(jié)構(gòu)體的RuntimeVisibleTypeAnnotations屬性中。
RuntimeInvisibleTypeAnnotations:與RuntimeVisibleTypeAnnotations相對,表示運(yùn)行時不可見注解
AnnotationDefault:位于method_info結(jié)構(gòu)的屬性表中,用來記錄注解類型的元素的默認(rèn)值
BootstrapMethods:位于ClassFile結(jié)構(gòu)的屬性表中,用于保存invokedynamic指令引用的引導(dǎo)方法限定符,如果常量池中包含CONSTANT_InvokeDynamic_info成員,則ClassFile的屬性表中必須包含且只能包含一個BootstrapMethods屬性。
MethodParameters:位于method_info結(jié)構(gòu)的屬性表中,用來記錄方法的形參的個數(shù),形參名,形參是否final等。
LineNumberTable 屬性
LineNumberTable 屬性 用于 Java 的調(diào)試,可指明某條指令對應(yīng)于源碼哪一行。
LineNumberTable 屬性用來描述 Java 源碼行號與字節(jié)碼行號之間的對應(yīng)關(guān)系。它默認(rèn)會生成到 Class 文件中,可以在 javac 中分別使用 -g:none 或者 -g:lines 來取消或者要求生成
LineNumberTable 屬性的結(jié)構(gòu)如下所示:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
其中最重要的是 line_number_table 數(shù)組,該數(shù)組元素包含如下 兩個成員變量:
- start_pc:為 code[] 數(shù)組元素的索引,用于指向 Code_attribute 中 code 數(shù)組某處指令。
- line_number:為 start_pc 對應(yīng)源文件代碼的行號。需要注意的是,多個 line_number_table 元素可以指向同一行代碼,因?yàn)橐恍?Java 代碼很可能被編譯成多條指令。
LocalVariableTable 屬性
LocalVariableTable 屬性用于 描述棧幀中局部變量表中的變量與 Java 源碼中定義的變量之間的關(guān)系,它也不是運(yùn)行時必需的屬性,但默認(rèn)會生成到 Class 文件之中, 可以在 javac 中分別使用 -g:none 或者 -g:vars 選項(xiàng)生成該信息。
LocalVariableTable 的數(shù)據(jù)結(jié)構(gòu)如下所示:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
其中最重要的元素是 local_variable_table 數(shù)組,其中的 start_pc 與 length 這兩個參數(shù) 決定了一個局部變量在 code 數(shù)組中的有效范圍。
需要注意的是,每個非 static 函數(shù)都會自動創(chuàng)建一個叫做 this 的本地變量,代表當(dāng)前是在哪個對象上調(diào)用此函數(shù)。
并且,this 對象是位于局部變量數(shù)組第1個位置(即 Slot = 0),它的作用范圍是貫穿整個函數(shù)的。
此外,在 JDK 1.5 引入泛型之后,LocalVariableTable 屬性增加了一個 “姐妹屬性”: LocalVariableTypeTable,這個新增的屬性結(jié)構(gòu)與 LocalVariableTable 非常相似,僅僅是把記錄 的字段描述符的 descriptor_index 替換成了字段的特征簽名(Signature),對于非泛型類型來 說,描述符和特征簽名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的參數(shù)化類型被擦除掉,描述符就不能準(zhǔn)確地描述泛型類型了,因此出現(xiàn)了 LocalVariableTypeTable。
Slot 是什么?
JVM 在調(diào)用一個函數(shù)的時候,會創(chuàng)建一個局部變量數(shù)組(即 LocalVariableTable),而 Slot 則表示當(dāng)前變量在數(shù)組中的位置。
字節(jié)碼
C/C++的方法會被編譯成特定于CPU架構(gòu)的匯編指令,然后交由CPU逐一執(zhí)行,因?yàn)閰R編指令與CPU架構(gòu)是強(qiáng)綁定的,所以C/C++程序在執(zhí)行前需要在不同CPU架構(gòu)的機(jī)器上編譯一遍。Java為了實(shí)現(xiàn)一處編譯,跨平臺運(yùn)行的目標(biāo),在匯編指令之上引入了一個獨(dú)立于平臺的中間層,虛擬機(jī)指令,由Java虛擬機(jī)規(guī)范提供指令標(biāo)準(zhǔn)定義,由Java虛擬機(jī)廠商提供指令實(shí)現(xiàn),不同平臺的Java虛擬機(jī)都遵循相同的指令集規(guī)范,從而實(shí)現(xiàn)跨平臺運(yùn)行目標(biāo)。一個方法對應(yīng)的一組虛擬機(jī)指令稱為這個方法的字節(jié)碼(byte codes)。
虛擬機(jī)指令定義
一條虛擬機(jī)指令由一個指定要完成的操作的操作碼和表示待操作值的的零或者多個操作數(shù)構(gòu)成,所謂操作數(shù)就是該指令的入?yún)?。操作?shù)可能在編譯器產(chǎn)生并嵌入到字節(jié)碼指令中,即在內(nèi)存中操作數(shù)緊挨著字節(jié)碼指令,也可能在運(yùn)行期通過計算得出保存在操作數(shù)棧中,這種情形下虛擬機(jī)指令的操作數(shù)的個數(shù)為0,直接從操作數(shù)棧中讀取操作數(shù),通常是棧頂?shù)腘個元素。所謂操作數(shù)棧就是保存虛擬機(jī)指令執(zhí)行結(jié)果的一個后進(jìn)先出的棧,由Java虛擬機(jī)維護(hù),其功能類似于匯編指令執(zhí)行過程中使用的進(jìn)程?;蛘邇?nèi)核棧。
Java虛擬機(jī)規(guī)范定義指令時使用了一張指令格式表,包含助記符,操作碼,指令格式,操作數(shù)棧,功能描述,鏈接時異常,運(yùn)行時異常,注意共8項(xiàng),class文件中的虛擬機(jī)指令就是數(shù)字形式的操作碼,助記符是對該數(shù)字的含義的簡單描述,指令格式描述該指令后面的操作數(shù),操作數(shù)棧描述該指令執(zhí)行時操作數(shù)棧的狀態(tài)和執(zhí)行完成操作數(shù)棧的狀態(tài),功能描述項(xiàng)會詳細(xì)描述該指令在各種場景下的處理邏輯,鏈接時異常項(xiàng)描述在鏈接環(huán)節(jié)執(zhí)行該指令校驗(yàn)時應(yīng)該拋出的異常類型,運(yùn)行時異常項(xiàng)描述了運(yùn)行時該指令執(zhí)行過程中應(yīng)該拋出的異常類型。
以無操作數(shù)的aload指令和有一個操作數(shù)的iload指令為例說明:

圖中的50是該指令的十進(jìn)制的操作碼,0x32是十六進(jìn)制的;圖中的箭頭表示棧頂,aload指令執(zhí)行時操作數(shù)棧的棧頂?shù)膬蓚€元素必須是arrayref和index,其中arrayref表示一個組件類型為reference的數(shù)組,即對象數(shù)組,是待讀取的目標(biāo)數(shù)組,index表示待讀取數(shù)組元素的索引,arrayref和index的類型校驗(yàn)在鏈接環(huán)節(jié)的類型檢查時完成。

iload的操作數(shù)index表示當(dāng)前棧幀中的局部變量的索引,iload指令將該索引處的int值壓入到操作數(shù)棧棧頂,index的值在編譯期確定并隨iload指令一起寫入到class文件中。
內(nèi)存表示
在class文件中,某個Java方法的虛擬機(jī)指令保存在表示該方法的method_info結(jié)構(gòu)的Code屬性的code項(xiàng)中,code項(xiàng)是一個byte數(shù)組,code_length項(xiàng)描述了該數(shù)組的長度。Java8定義的虛擬機(jī)指令總共有204個,其中3個是Java虛擬機(jī)內(nèi)部使用的保留指令,因此指令操作碼用一個字節(jié)即可表示,每個指令后面的字節(jié)按照規(guī)范表示該指令的操作數(shù),操作數(shù)的類型和字節(jié)數(shù)在規(guī)范中都有定義(指令格式表中格式項(xiàng)每一行都表示一個字節(jié)),Java方法對應(yīng)的一組指令按照指令執(zhí)行的先后順序依次保存,code[0]就是第一條執(zhí)行的指令。解析code數(shù)組包含的虛擬機(jī)指令時,先從code[0]讀取第一條指令,判斷其是否有操作數(shù)及其占用的字節(jié)數(shù),如果有則讀取指定字節(jié)數(shù)的操作數(shù),如果沒有或者操作數(shù)讀取完畢則下一個字節(jié)是下一條虛擬機(jī)指令,依次往下循環(huán)即可,在class文件校驗(yàn)環(huán)節(jié)會檢查code數(shù)組是否合法完整。
3、運(yùn)行邏輯
在《Hotspot Klass模型——Java類內(nèi)存表示機(jī)制》中講到方法的字節(jié)碼實(shí)際保存在ConstMethod對象的后面,可以通過HSDB具體的字節(jié)碼及其內(nèi)存數(shù)據(jù),測試代碼:
package jvmTest;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
public class MainTest4 {
public static void main(String[] args) {
test();
while (true) {
try {
System.out.println(getProcessID());
Thread.sleep(600 * 1000);
} catch (Exception e) {
}
}
}
private static void test(){
int a=1;
int a2=2;
int a3=3;
int[] b={a,a2,a3};
b[2]=4;
int c=b[2];
test2(c);
}
private static void test2(int c){
System.out.println(c);
}
public static final int getProcessID() {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
System.out.println(runtimeMXBean.getName());
return Integer.valueOf(runtimeMXBean.getName().split("@")[0])
.intValue();
}
}
其中l(wèi)ine表示行號,bci表示該字節(jié)碼在code數(shù)組中的索引。再用Inspect,根據(jù)方法的地址找到該方法的ConstMethod對象的地址,如下圖:

接著用CLHSDB的mem命令查看ConstMethod對象的內(nèi)存,不過先得執(zhí)行printas命令查看該對象的內(nèi)存大小,從而確定該方法字節(jié)碼在內(nèi)存中的起始位置,如下圖:

該對象的大小是48字節(jié),即6個字段,從第7個字寬開始即是字節(jié)碼的內(nèi)存起始位置。字節(jié)碼的內(nèi)存大小是37字節(jié),即5個字寬,用mem命令查看這5個字寬的內(nèi)存數(shù)據(jù),如下圖:

Intel按照小端的方式存儲數(shù)據(jù),因?yàn)閙em命令打印內(nèi)存數(shù)據(jù)時做了一道轉(zhuǎn)換,所以實(shí)際的內(nèi)存數(shù)據(jù)與上圖打印的結(jié)果是相反的,以第一行0xbc 06 3d 06 3c 05 3b 04為例,實(shí)際存儲和讀取的順序是從04開始的,從右往左,一直到bc,然后從下一行的末尾的0a開始。將內(nèi)存數(shù)據(jù)與上面的虛擬機(jī)指令的操作碼逐一比對,結(jié)果如下:
iconst_1 04 將int變量1壓入棧頂
istore_0 3b 將棧頂?shù)膇nt變量放入局部變量表索引為0的位置,即變量a的初始化
iconst_2 05 將int變量2壓入棧頂
istore_1 3c 將棧頂?shù)膇nt變量放入局部變量表索引為1的位置,即變量a2的初始化
iconst_3 06
istore_2 3d 變量a3的初始化
iconst_3 06 將int變量3壓入棧頂,3是數(shù)組的初始化大小
newarray int bc 0a 初始化數(shù)組,0a表示數(shù)組的元素類型
dup 59 復(fù)制棧頂?shù)牟僮鲾?shù)并壓入棧頂,此時棧頂?shù)牟僮鲾?shù)是newarray創(chuàng)建的int數(shù)組的引用,dup執(zhí)行完成棧頂頭2個元素 就是該引用
iconst_0 03 將int變量0壓入棧頂
iload_0 1a 將局部變量表中索引為0的變量壓入棧頂,即變量a,執(zhí)行完的狀態(tài)如下圖
iastore 4f 讀取棧頂?shù)娜齻€操作數(shù),將指定value插入到指定數(shù)組的指定索引處,然后三個操作數(shù)彈出,即變量a插入int數(shù)組b 索引0處
dup 59 此時棧頂還是int數(shù)組b的引用,執(zhí)行完后棧頂頭兩個元素都是該引用
iconst_1 04
iload_1 1b
iastore 4f 同上,將變量a2插入int數(shù)組b索引1處
dup 59
iconst_2 05
iload_2 1c
iastore 4f 同上,將變量a3插入int數(shù)組b索引1處,此時棧頂?shù)臄?shù)據(jù)是int數(shù)組b的引用
astore_3 4e 將棧頂?shù)囊脭?shù)據(jù)保存到局部變量表索引為3的地方,前面0,1,2分別是變量a,a2,a3
aload_3 2d 從局部變量表加載索引為3的引用變量到棧頂,即int數(shù)組b的引用重新放入棧頂
iconst_2 05 將int變量2壓入棧頂,即待讀取的數(shù)組索引2
iconst_4 07 將int變量4壓入棧頂,此時操作數(shù)棧的狀態(tài)如下圖。
iastore 4f 讀取棧頂?shù)娜齻€元素,將指定數(shù)組的指定索引處的值修改為目標(biāo)值,即int數(shù)組b索引為2的元素修改為4
aload_3 2d int數(shù)組b的引用重新放入棧頂
iconst_2 05 將int變量2壓入棧頂,即待讀取的數(shù)組索引
iaload 2e 讀取棧頂?shù)念^兩個元素,讀取指定數(shù)組的指定索引的元素并加載到棧頂,此時操作數(shù)棧只有一個該元素
istore #4 36 04 將棧頂int元素放入局部變量表索引為4的地方
iload #4 e0 04 讀取局部變量表索引為4的元素到棧頂
invokestatic #10 b8 06 00 調(diào)用static方法,test2,后面的06和00用于計算目標(biāo)方法的在常量池的方法描述符的索引
return b1 返回到調(diào)用該方法的指令處

javap命令
上節(jié)中我們使用HSDB查看方法字節(jié)碼,除此之外,Java還提供了解析class文件的javap命令,javap會將其中的二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成方便閱讀的文本數(shù)據(jù),可以借此具體了解Class文件格式各部分的構(gòu)成。不過javap對class文件有要求,如果需要輸出完整的信息,在javac編譯源代碼文件時需要加上-g選項(xiàng),類似于gcc的-g選項(xiàng),此選項(xiàng)會加上額外的源碼的相關(guān)信息。
1、命令選項(xiàng)
如下圖,注意-c并不是反匯編,只是查看class文件中的方法字節(jié)碼,-v是輸出最完整全面的信息

2、命令輸出說明
測試代碼如下:
package jvmTest;
import org.junit.runner.RunWith;
import org.junit.runners.BlockJUnit4ClassRunner;
import javax.xml.bind.annotation.XmlElement;
class MainBase{
public void show(){
System.out.println("MainBase");
}
}
@RunWith(BlockJUnit4ClassRunner.class)
public class MainTest4 extends MainBase implements Runnable {
@XmlElement(name = "ax")
private int a=12;
private long l=13;
private static final String s="test";
@Override
public void run() {
show();
}
public void test(int a) throws NullPointerException {
try {
int a2=2;
int a3=3;
int[] b={a,a2,a3};
b[2]=4;
int c=b[2];
test2(new myInner(c));
} catch (NullPointerException e) {
throw e;
}
}
public static void test2(myInner inner){
System.out.println(inner.getA());
}
@Override
public void show(){
System.out.println("MainTest4");
}
class myInner{
private int a;
public myInner(int a) {
this.a = a;
}
public int getA(){
return a;
}
}
}
代碼中使用了Junit包,為了解決使用javac編譯找不到Junit相關(guān)類的問題,可利用ideal完成該類的編譯,在Java Compile里面加上-g選項(xiàng)即可,如下圖:

編譯完成后,進(jìn)入target/test-class目錄下,執(zhí)行javap -v MainTest4.class > javap.txt即可,結(jié)果如下:
Classfile /D:/git/MyJavaTest/target/test-classes/jvmTest/MainTest4.class
Last modified 2019-8-18; size 1616 bytes
MD5 checksum 7b6af831678f64966f42e7b998f3a374
Compiled from "MainTest4.java"
public class jvmTest.MainTest4 extends jvmTest.MainBase implements java.lang.Runnable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
//常量池表
Constant pool:
//#1表示常量池的索引,Methodref表示該項(xiàng)的類型,即CONSTANT_Fieldref_info,#17.#63實(shí)際是該類型的兩個屬性,class_index和name_and_type_index
//最后的//的內(nèi)容是對該屬性的翻譯
#1 = Methodref #17.#63 // jvmTest/MainBase."<init>":()V
#2 = Fieldref #16.#64 // jvmTest/MainTest4.a:I
#3 = Long 13l
#5 = Fieldref #16.#65 // jvmTest/MainTest4.l:J
#6 = Methodref #16.#66 // jvmTest/MainTest4.show:()V
#7 = Class #67 // jvmTest/MainTest4$myInner
#8 = Methodref #7.#68 // jvmTest/MainTest4$myInner."<init>":(LjvmTest/MainTest4;I)V
#9 = Methodref #16.#69 // jvmTest/MainTest4.test2:(LjvmTest/MainTest4$myInner;)V
#10 = Class #70 // java/lang/NullPointerException
#11 = Fieldref #71.#72 // java/lang/System.out:Ljava/io/PrintStream;
#12 = Methodref #7.#73 // jvmTest/MainTest4$myInner.getA:()I
#13 = Methodref #74.#75 // java/io/PrintStream.println:(I)V
#14 = String #76 // MainTest4
#15 = Methodref #74.#77 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #78 // jvmTest/MainTest4
#17 = Class #79 // jvmTest/MainBase
#18 = Class #80 // java/lang/Runnable
#19 = Utf8 myInner
#20 = Utf8 InnerClasses
#21 = Utf8 a
#22 = Utf8 I
#23 = Utf8 RuntimeVisibleAnnotations
#24 = Utf8 Ljavax/xml/bind/annotation/XmlElement;
#25 = Utf8 name
#26 = Utf8 ax
#27 = Utf8 l
#28 = Utf8 J
#29 = Utf8 s
#30 = Utf8 Ljava/lang/String;
#31 = Utf8 ConstantValue
#32 = String #41 // test
#33 = Utf8 <init>
#34 = Utf8 ()V
#35 = Utf8 Code
#36 = Utf8 LineNumberTable
#37 = Utf8 LocalVariableTable
#38 = Utf8 this
#39 = Utf8 LjvmTest/MainTest4;
#40 = Utf8 run
#41 = Utf8 test
#42 = Utf8 (I)V
#43 = Utf8 a2
#44 = Utf8 a3
#45 = Utf8 b
#46 = Utf8 [I
#47 = Utf8 c
#48 = Utf8 e
#49 = Utf8 Ljava/lang/NullPointerException;
#50 = Utf8 StackMapTable
#51 = Class #70 // java/lang/NullPointerException
#52 = Utf8 Exceptions
#53 = Utf8 test2
#54 = Utf8 (LjvmTest/MainTest4$myInner;)V
#55 = Utf8 inner
#56 = Utf8 LjvmTest/MainTest4$myInner;
#57 = Utf8 show
#58 = Utf8 SourceFile
#59 = Utf8 MainTest4.java
#60 = Utf8 Lorg/junit/runner/RunWith;
#61 = Utf8 value
#62 = Utf8 Lorg/junit/runners/BlockJUnit4ClassRunner;
#63 = NameAndType #33:#34 // "<init>":()V
#64 = NameAndType #21:#22 // a:I
#65 = NameAndType #27:#28 // l:J
#66 = NameAndType #57:#34 // show:()V
#67 = Utf8 jvmTest/MainTest4$myInner
#68 = NameAndType #33:#81 // "<init>":(LjvmTest/MainTest4;I)V
#69 = NameAndType #53:#54 // test2:(LjvmTest/MainTest4$myInner;)V
#70 = Utf8 java/lang/NullPointerException
#71 = Class #82 // java/lang/System
#72 = NameAndType #83:#84 // out:Ljava/io/PrintStream;
#73 = NameAndType #85:#86 // getA:()I
#74 = Class #87 // java/io/PrintStream
#75 = NameAndType #88:#42 // println:(I)V
#76 = Utf8 MainTest4
#77 = NameAndType #88:#89 // println:(Ljava/lang/String;)V
#78 = Utf8 jvmTest/MainTest4
#79 = Utf8 jvmTest/MainBase
#80 = Utf8 java/lang/Runnable
#81 = Utf8 (LjvmTest/MainTest4;I)V
#82 = Utf8 java/lang/System
#83 = Utf8 out
#84 = Utf8 Ljava/io/PrintStream;
#85 = Utf8 getA
#86 = Utf8 ()I
#87 = Utf8 java/io/PrintStream
#88 = Utf8 println
#89 = Utf8 (Ljava/lang/String;)V
//方法表
{
public jvmTest.MainTest4();
//方法描述符
descriptor: ()V
//訪問標(biāo)識
flags: ACC_PUBLIC
//method_info結(jié)構(gòu)的Code屬性
Code:
//stack表示操作數(shù)棧的最大深度,locals表示本地變量的個數(shù),args_size表示參數(shù)個數(shù)
stack=3, locals=1, args_size=1
//方法字節(jié)碼
0: aload_0
1: invokespecial #1 // Method jvmTest/MainBase."<init>":()V
4: aload_0
5: bipush 12
7: putfield #2 // Field a:I
10: aload_0
11: ldc2_w #3 // long 13l
14: putfield #5 // Field l:J
17: return
//虛擬機(jī)指令對應(yīng)的代碼行號
LineNumberTable:
line 15: 0
line 17: 4
line 19: 10
//本地變量表,this是默認(rèn)傳入方法的參數(shù)
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this LjvmTest/MainTest4;
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #6 // Method show:()V
4: return
LineNumberTable:
line 24: 0
line 25: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LjvmTest/MainTest4;
public void test(int) throws java.lang.NullPointerException;
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=4, locals=6, args_size=2
0: iconst_2
1: istore_2
2: iconst_3
3: istore_3
4: iconst_3
5: newarray int
7: dup
8: iconst_0
9: iload_1
10: iastore
11: dup
12: iconst_1
13: iload_2
14: iastore
15: dup
16: iconst_2
17: iload_3
18: iastore
19: astore 4
21: aload 4
23: iconst_2
24: iconst_4
25: iastore
26: aload 4
28: iconst_2
29: iaload
30: istore 5
32: new #7 // class jvmTest/MainTest4$myInner
35: dup
36: aload_0
37: iload 5
39: invokespecial #8 // Method jvmTest/MainTest4$myInner."<init>":(LjvmTest/MainTest4;I)V
42: invokestatic #9 // Method test2:(LjvmTest/MainTest4$myInner;)V
45: goto 51
48: astore_2
49: aload_2
50: athrow
51: return
//方法中捕獲的異常,try/catch的范圍0-45,異常類型NullPointerException
Exception table:
from to target type
0 45 48 Class java/lang/NullPointerException
LineNumberTable:
line 29: 0
line 30: 2
line 31: 4
line 32: 21
line 33: 26
line 34: 32
line 37: 45
line 35: 48
line 36: 49
line 38: 51
LocalVariableTable:
Start Length Slot Name Signature
2 43 2 a2 I
4 41 3 a3 I
21 24 4 b [I
32 13 5 c I
49 2 2 e Ljava/lang/NullPointerException;
0 52 0 this LjvmTest/MainTest4;
0 52 1 a I
//將異常重新拋出使用
StackMapTable: number_of_entries = 2
frame_type = 112 /* same_locals_1_stack_item */
stack = [ class java/lang/NullPointerException ]
frame_type = 2 /* same */
//此方法可能拋出的異常類型
Exceptions:
throws java.lang.NullPointerException
public static void test2(jvmTest.MainTest4$myInner);
descriptor: (LjvmTest/MainTest4$myInner;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #12 // Method jvmTest/MainTest4$myInner.getA:()I
7: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
10: return
LineNumberTable:
line 41: 0
line 42: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 inner LjvmTest/MainTest4$myInner;
public void show();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #14 // String MainTest4
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 46: 0
line 47: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LjvmTest/MainTest4;
}
//ClassFile結(jié)構(gòu)的屬性表
//源代碼文件名
SourceFile: "MainTest4.java"
//運(yùn)行時可見的注解,#60是注解的類名,#61是屬性名,c#62是屬性值
RuntimeVisibleAnnotations:
0: #60(#61=c#62)
//該類的內(nèi)部類
InnerClasses:
#19= #7 of #16; //myInner=class jvmTest/MainTest4$myInner of class jvmTest/MainTest4
javap命令不支持fileds字段表和interfaces接口表的打印,可參考OpenJDK langtools包下javap命令的實(shí)現(xiàn),如下圖:
