深入理解JVM-java字節碼文件結構剖析(1)
public class MyTest1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
javap -verbose MyTest1 警告: 二進制文件MyTest1包含jvm.bytecode.MyTest1 Classfile /Users/luozhiyun/Documents/work/jvm_lecture/target/classes/jvm/bytecode/MyTest1.class Last modified Mar 14, 2019; size 471 bytes MD5 checksum b2dc69fae4f63b54509ddc1a9210e9c3 Compiled from "MyTest1.java" public class jvm.bytecode.MyTest1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // jvm/bytecode/MyTest1.a:I #3 = Class #22 // jvm/bytecode/MyTest1 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Ljvm/bytecode/MyTest1; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest1.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 jvm/bytecode/MyTest1 #23 = Utf8 java/lang/Object { public jvm.bytecode.MyTest1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 6: 0 line 8: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Ljvm/bytecode/MyTest1; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 11: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ljvm/bytecode/MyTest1; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 15: 0 line 16: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Ljvm/bytecode/MyTest1; 0 6 1 a I } SourceFile: "MyTest1.java" 對應的16進制 cafe babe 魔數:所有的.class字節碼文件的前4個字節都是魔數,魔數值為固定值:OxCAFEBABE 0000 0034 魔數之後的4個字節為版本信息,前兩個字節表示minor version(次版本號),後兩個字節表示major version(主版本號)。這裏的版本號為00 00 00 34 ,換算成十進制,表示次版本號為0,主版本號為52。所以,該文件的版本號為:1.8.0 0018 表示一共有24個常量 0a 代表值為10 Methodref_info 這個常量 00 04 00 14 代表4 和 20 即上面反編譯的結果 #4.#20 09 表示Fieldref 0003 0015 表示3 和 21 即上面反編譯的結果 #3.#21 07 表示Class 00 16 代表 #22 07 表示Class 00 17 代表 #23 01 表示utf8 00 01 長度為1 61 代表a 01 表示utf8 00 01 長度為1 49 代表I 0c 表示NameAndType 00 07 表示指向該字段或方法名稱常量項的索引 #7: 00 08 表示指向該字段或方法描述符常量項的索引 #8 0c 00 05 表示指向該字段或方法名稱常量項的索引 #5 00 06 表示指向該字段或方法描述符常量項的索引 #6 Access flag 00 21 表示ACC_PUBLIC 與ACC_SUPER取的並集 This Class Name 00 03 表示的一個索引指的是常量池第三個常量 This Super Name 00 04 表示的一個索引指的是常量池第四個常量 Interfaces 00 00 表示沒有實現接口 Fields 00 01 表示屬性的數量,表示有一個字段 00 02 access_flag 表示private 00 05 name_index 名字的索引 00 06 descriptor_index 描述符的索引 00 00 表示attributes_count為0,也就沒有attributes_info Method 00 03 代表有三個方法,包含了自動生成的默認構造器 00 01 代表是一個public的方法 00 07 代表是name_index <init> 00 08 代表是descriptor_index. ()V 00 01 代表是attribute_count,有一個屬性 method attribute 00 09 代表屬性名的索引attribute_name_index code 00 00 00 38 attribute_length 56個字節長度 00 02 max_stack 00 01 max_local 局部變量最大值 00 00 00 0a code_length 方法的代碼長度 10個字節,代表這個方法所真正運行的字節碼 2a b7 00 01 2a 04 b5 00 02 b1 實際上就是對應著下面的助記符 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return 2a -> aload_0 b7 -> invokespecial 04 -> aload_0 b5 -> putfield (為成員變量設置,通過這個變量我們可以知道,成員變量其實是在構造器中賦值的) b1 -> return exception 00 00 attributes 00 02 attribute_count; 表示有兩個屬性 00 0a 屬性的索引 #10 LineNumberTable 00 00 00 0a attribute_length 10個字節 00 02 00 00 00 06 00 04 00 08 00 02 表示兩兩映射 00 00 -> 00 06 表示偏移量為0映射到行號為6 00 04 -> 00 08 表示偏移量為4映射到行號為8 00 0b 第二個屬性的索引 #11 LocalVariableTable 00 00 00 0c 表示LocalVariableTable所占的字節長度 00 01 00 00 00 0a 00 0c 00 0d 00 00 00 01 局部變量的個數 00 00 局部變量開始的位置 00 0a 局部變量結束的位置 00 0c 局部變量索引 #12 this 00 0d 索引 #13 Ljvm/bytecode/MyTest1; 00 00 用來做校驗檢查的 第二個方法 00 01 訪問修飾符 代表public 00 0e 方法的名字索引 getA 00 0f 方法的描述符的索引 ()I 00 01 代表是attribute_count,有一個屬性 00 09 代表屬性名的索引attribute_name_index code 00 00 00 2f 屬性的長度47 00 01 max_stack 00 01 max_local 局部變量的數量是1 00 00 00 05 code_length 方法的代碼長度 5個字節 2a b4 00 02 ac 方法的執行體 2a -> aload_0 b4 -> getField. 00 02 表示常量池中的第二個常量 #2 ac -> ireturn 表示返回一個整型 exception 00 00 attributes 00 02 attribute_count; 表示有兩個屬性 00 0a 索引指向第10個常量 LineNumberTable 00 00 00 06 attribute_length 6個字節 00 01 00 00 00 0b 表示只有一行, 偏移量為零對應行號8 00 0b 第二個屬性的索引 #11 LocalVariableTable 00 00 00 0c 表示LocalVariableTable所占的字節長度 12個 00 01 00 00 00 05 00 0c 00 0d 00 00 00 01 局部變量的個數 00 00 局部變量開始的位置 00 05 局部變量結束的位置 00 0c 局部變量索引的位置 #12 this 00 0d 索引 #13 Ljvm/bytecode/MyTest1; 00 00 用來做校驗檢查的 第三個方法 00 01 表示public 00 10 表示索引#16 setA 00 11 表示索引#17 (I)V 00 01 代表是attribute_count,有一個屬性 00 09 代表屬性名的索引attribute_name_index code 00 00 00 3e code_length 方法的代碼長度 62個字節 00 02 max_stack 00 02 max_local 00 00 00 06 code_length 方法的代碼長度 6個字節 2a 1b b5 00 02 b1 2a -> aload_0 1b -> iload_1 b5 -> putfield 00 02 表示第二常量 b1 -> return attributes 00 02 attribute_count; 表示有兩個屬性 00 0a 索引指向第10個常量 LineNumberTable 00 00 00 0a attribute_length 10個字節 00 02 00 00 00 0f 00 05 00 10 00 02 表示有兩個對應關系 00 00 00 0f 偏移量為0的對應的是12行 00 05 00 10 偏移量為5的對應的是16行 00 0b 第二個屬性的索引 #11 LocalVariableTable 00 00 00 16 表示LocalVariableTable所占的字節長度 22個 00 02 00 00 00 06 00 0c 00 0d 00 00 00 00 00 06 00 05 00 06 00 01 00 02 局部變量的個數 00 00 局部變量開始的位置 00 06 局部變量結束的位置 00 0c 局部變量索引的位置 #12 this 00 0d 索引 #13 Ljvm/bytecode/MyTest1; 00 00 局部變量開始的位置 00 06 局部變量結束的位置 00 05 局部變量索引的位置 #5 a 00 06 索引#6 I Attributes 00 01 00 12 索引 #18 sourceFile 00 00 00 02 源文件的長度 00 13 索引#19 MyTest1.java cafe babe 0000 0034 0018 0a00 0400 1409 0003 0015 0700 1607 0017 0100 0161 0100 0149 0100 063c 696e 6974 3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 0100 124c 6f63 616c 5661 7269 6162 6c65 5461 626c 6501 0004 7468 6973 0100 164c 6a76 6d2f 6279 7465 636f 6465 2f4d 7954 6573 7431 3b01 0004 6765 7441 0100 0328 2949 0100 0473 6574 4101 0004 2849 2956 0100 0a53 6f75 7263 6546 696c 6501 000c 4d79 5465 7374 312e 6a61 7661 0c00 0700 080c 0005 0006 0100 146a 766d 2f62 7974 6563 6f64 652f 4d79 5465 7374 3101 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 0021 0003 0004 0000 0001 0002 0005 0006 0000 0003 0001 0007 0008 0001 0009 0000 0038 0002 0001 0000 000a 2ab7 0001 2a04 b500 02b1 0000 0002 000a 0000 000a 0002 0000 0006 0004 0008 000b 0000 000c 0001 0000 000a 000c 000d 0000 0001 000e 000f 0001 0009 0000 002f 0001 0001 0000 0005 2ab4 0002 ac00 0000 0200 0a00 0000 0600 0100 0000 0b00 0b00 0000 0c00 0100 0000 0500 0c00 0d00 0000 0100 1000 1100 0100 0900 0000 3e00 0200 0200 0000 062a 1bb5 0002 b100 0000 0200 0a00 0000 0a00 0200 0000 0f00 0500 1000 0b00 0000 1600 0200 0000 0600 0c00 0d00 0000 0000 0600 0500 0600 0100 0100 1200 0000 0200 13
使用javap -verbose 命令分析一個字節碼文件時,將會分析該字節碼文件的魔數、版本號、 常量池、類的構造方法、類中的方法信息、類變量與成員變量等信息。
- 魔數:所有的.class字節碼文件的前4個字節都是魔數,魔數值為固定值:OxCAFEBABE
- 魔數之後的4個字節為版本信息,前兩個字節表示minor version(次版本號),後兩個字節表示major version(主版本號)。這裏的版本號為00 00 00 34 ,換算成十進制,表示次版本號為0,主版本號為52。所以,該文件的版本號為:1.8.0
- 常量池(content pool):緊接著主版本號之後的就是常量池入口。一個java類中定義的很多信息都是由常量池來維護和描述的。可以將常量池看坐是Class文件的資源倉庫,比如說java類中定義的方法與變量信息,都是存儲在常量池中。常量池中主要存儲兩類常量:字面量與符號引用。字面量如文本字符串,java中聲明為final的常量值等,而符號引用如類和接口的全局限定名,字段的名稱和描述符,方法的名稱和描述符等。
- 常量池的總體結構:java類所對應的常量池主要由常量池數量與常量池數組這兩部分共同構成。常量池數量緊跟在主版本號後面,占據2個字節;常量池數組則緊跟在常量池數量之後。常量池數組與一般的數組不同的是,常量池數組中不同的元素的類型、結構都是不同的,長度當然也就不同;但是,每一種元素的第一個數據都是一個u1類型,該字節是個標誌位,占據1個字節。jvm在解析常量池時,會根據這個u1類型來獲取元素的具體類型。值得註意的是,常量池數組中元素的個數 = 常量池數 -1 (其中0暫時不適用),目的是滿足某些常量池索引值的數據在特定情況下需要表達「不引用任何一個常量池」的含義;根本原因在於,索引為0也是一個常量(保留常量),只不過它部位與常量表中,這個常量就對應null值;所以,常量池的索引從1而非0開始。
- 在jvm規範中,每個變量/字段都有描述信息,描述信息主要的作用是描述字段的數據類型、方法的參數列表(包含數量、類型與順序)與返回值。根據描述符規則,基本數據類型和代表無返回值的void類型都用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示。為了壓縮字節碼文件的體積,對於基本數據類型,jvm都只使用一個大寫字母來表示,如下所示:B- byte , C - char, D - double, F -float, I - int, J -long, S - short , Z - boolean, V - void, L -對象類型,如Ljava/lang/String;
- 對於數組類型來說,每一個維度使用一個前置的[來表示,如int[]被記錄為[I,String[][]被記錄為[[Ljava/lang/String;
用描述符來描述方法時,按照先參數列表,後返回值的順序來描述,參數列表按照參數的嚴格順序放在一組()之內,如方法:String getRealnamByIdANdNickName(int id,String name)的描述符為:(I,Ljava/lang/String;)Ljava/lang/String;
Java字節碼整體結構
- 4個字節 Magic Number 魔數值為OxCAFEBABE
- 2+2個字節 Version 前兩個字節表示minor version(次版本號),後兩個字節表示major version(主版本號)。1.1(45),1.2(46),1.3(47),1.4(48),1.5(49),1.6(50),1.7(51)
- 2+ n個字節 Constant Pool 包括字符串常量、數值常量等
- 2個字節 Access Flags 訪問標誌(public class 、private class 等)
- 2個字節 This Class Name
- 2個字節 Super Class Name
- 2+n個字節 Interfaces
- 2+n個字節 Fields 當前這個類的成員變量的信息
- 2+n個字節 Methods
- 2+n個字節 Attributes 當前這個類的附加的屬性
Class字節碼中有兩種數據類型
* 字節數據直接量:這事基本的數據類型。共細分為u1、u2、u4、u8四種,分別代表連續的1個字節、2個字節、4個字節、8個字節組成的整體數據
* 表(數組):表是由多個基本數據或其他表,按照既定順序組成的大的數據集合。表是有結構的,它的結構體現在:組成表的成分所在的位置和順序都是已經嚴格定義好的。
ClssFile{
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_pool[constant_pool_count -1];
u2 access_flags;
u2 this.class;
u2 super.class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 method_count;
method_info methos[method_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Access_Flag訪問標誌
訪問標誌信息包括該Class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,是否被聲明成final。通過上面的源代碼,我們知道該文件是類並且是public。
字段表集合
字段表用於描述類和接口中聲明的變量。這裏的字段包含了類基表變量以及實例變量,但是不包括方法內部聲明的局部變量。
field_info{
u2 access_flags; 0002
u2 name_index; 0005
u2 descriptor_index; 0006
u2 attributes_count; 0000
attribute_info attributes[attributes_count];
}
方法表
前三個字段和field_info一樣
method_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法的屬性結構
方法中的每個屬性都是一個attribute_info結構
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
* jvm預定義了部分attribute,但是編譯器自己也可以實現自己的attribute寫入class文件裏,供運行時使用。
* 不同的attribute通過attribute_name_index來區分
Code結構
Code attribute的作用是保存該方法的結構,如所對應的字節碼
Code_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attribute_count;
attribute_info attributes[attribute_count];
}
* attribute_length表示attribute所包含的字節數,不包含attribute_name_index和attribute_length字段
* max_stack表示這個方法運行的任何時刻所能達到的操作數棧的最大深度
* max_locals表示方法執行期間創建的局部變量的數目,包含用來表示傳入的參數的局部變量
* code_length表示該方法所包含的字節碼的字節數以及具體的指令碼
* 具體字節碼即是該方法被調用時,虛擬機所執行的字節碼
* exception_table,這裏存放的是處理異常的信息
* 每個exception_table表項由start_pc, end_pc, handler_pc, catch_type組成
* start_pc和end_pc表示在code數組中的從start_pc到end_pc處(包含start_pc,不包含end_pc)的指令拋出的異常會由這個表項來處理
* handler_pc表示處理異常的代碼的開始處。catch_type表示會被處理的異常類型,它指向常量池的一個異常類。當catch_type為0時,表示處理所有的異常
附加屬性
接下在是該方法的附加屬性
LineNumberTable:這個屬性用來表示code數組中的字節碼和java代碼行數之間的關系。這個屬性可以用來在調試的時候定位代碼執行的行數
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];
}
上面的信息可以用jClasslib這樣的一個工具來查看
深入理解JVM-java字節碼文件結構剖析(1)