Java 字節碼文件結構

對於jvm來說,輸入的是class字節碼文件。而不是java源碼文件。java的執行過程是通過編譯器將java源碼文件編譯為class文件。在通過jvm中的編譯器將源碼編譯為機器碼進行執行

1 字節碼文件格式

《Java 虛擬機規範》規定了 Java 虛擬機結構、Class 類文件結構、字節碼指令等內容。字節碼文件結構是一組以 8 位字節為基礎的二進制流,各數據項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有添加任何分隔符。在字節碼結構中,有兩種最基本的數據類型來表示字節碼文件格式,分別是:無符號數和表。

無符號數屬於最基本的數據類型它以 u1、u2、u4、u8 六七分別代表 1 個字節、2 個字節、4 個字節、8 個字節的無符號數。無符號數可以用來描述數字、索引引用、數量值或者按照 UTF-8 編碼構成的字符串值。例如下表中第一行中的 u4 表示 Class 文件前 4 個字節表示該文件的魔數,第二行的 u2 表示該 Class 文件第 5-6 個字節表示該 JDK 的次版本號。

表是由多個無符號數或者其他表作為數據項構成的複合數據類型。所有表都習慣性地以_info結尾。表用於描述有層次關係的複合結構的數據,例如下表第 5 行表示其實一個類型為 cp_info 的表(常量池),這裡面存儲了該類的所有常量

Java 字節碼文件結構

2 具體探索

參考上面的表格可以將一個完整的class文件劃分為7個模塊:

  • 魔數和class的版本
  • 常量池
  • 訪問標識
  • 類索引,父類索引,接口索引
  • 字段集合
  • 方法集合
  • 屬性集合

首先編寫一段代碼,輸出簡單的hello world字符串。代碼內容如下

public class Test{
 public static void main(String args[]){
 String a = "Hello Wolrd.";
 System.out.println(a);
 }
}

使用java自帶的編譯命令來對這段代碼進行編譯。編譯命令: javac Test.java

cafe babe 0000 0034 001d 0a00 0600 0f08
0010 0900 1100 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 0009 5465 7374 2e6a
6176 610c 0007 0008 0100 0c48 656c 6c6f
2057 6f6c 7264 2e07 0017 0c00 1800 1907
001a 0c00 1b00 1c01 0004 5465 7374 0100
106a 6176 612f 6c61 6e67 2f4f 626a 6563
7401 0010 6a61 7661 2f6c 616e 672f 5379
7374 656d 0100 036f 7574 0100 154c 6a61
7661 2f69 6f2f 5072 696e 7453 7472 6561
6d3b 0100 136a 6176 612f 696f 2f50 7269
6e74 5374 7265 616d 0100 0770 7269 6e74
6c6e 0100 1528 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0021 0005 0006
0000 0000 0002 0001 0007 0008 0001 0009
0000 001d 0001 0001 0000 0005 2ab7 0001
b100 0000 0100 0a00 0000 0600 0100 0000
0100 0900 0b00 0c00 0100 0900 0000 2b00
0200 0200 0000 0b12 024c b200 032b b600
04b1 0000 0001 000a 0000 000e 0003 0000
0003 0003 0004 000a 0005 0001 000d 0000
0002 000e 

2.1 魔數與Class文件版本

Class 文件的第 1 - 4 個字節代表了該文件的魔數(Magic Number)。它唯一的作用是確定這個文件是否為一個能被虛擬機接受的 Class 文件,其值固定是:0xCAFEBABE(咖啡寶貝)。如果一個 Class 文件的魔數不是 0xCAFEBABE,那麼虛擬機將拒絕運行這個文件。

Class 文件的第 5 - 6 個字節代表了 Class 文件的次版本號(Minor Version),即編譯該 Class 文件的 JDK 次版本號。

Class 文件的第 7 - 8 個字節代表了 Class 文件的主版本號(Major Version),即編譯該 Class 文件的 JDK 主版本號。

Java 字節碼文件結構

從上面的class文件中看到。對應的魔數為cafe babe.次版本 0000 主版本0034.也就是通過JDK1.8編譯的

2.2 常量池

緊跟版本信息之後的是常量池信息,其中前 2 個字節表示常量池個數,其後的不定長數據則表示常量池的具體信息。從版本信息後面的內容為 001d。這個轉換為十進制表示29。查class文件格式說明中的表格可知道:當前常量共計 29 -1 = 28個。

Java 字節碼文件結構

2.2.1 分析常量

從001d以後是常量信息。一共28個。根據上面的表可以看到每個常量都有一個tag用於表示常量的類型。佔用1個字節。根據tag對應的具體值,在查表後看具體該常量後面有幾屬性,分別佔用多少字節。由此可以看到第一個常量是

0a00 0600 0f

第一個字節0a對應的十進制是10。查表可以對應的是CONSTANT_Methodref_info常量。具體的信息如下根據結構整理後如下:

-- 第1個常量
CONSTANT_Methodref_info {
 u1 tag = 0x0a;
 u2 class_index = 0x0006;
 u2 name_and_type_index = 0x000f;
}
-- 第2個常量
CONSTANT_String_info {
 u1 tag = 0x08;
 u8 length = 0x0010;
}
-- 第3個常量
CONSTANT_Fieldref_info {
 u1 tag = 0x09;
 u2 class_index = 0x0011
 u2 name_and_type_index = 0x0012;
}
-- 第4個常量
CONSTANT_Methodref_info {
 u1 tag = 0x0a;
 u2 class_index = 0x0013
 u2 name_and_type_index = 0x0014;
}
-- 第5個常量
CONSTANT_Class_info {
 u1 tag = 0x07;
 u2 utf8_index = 0x0015;
}
-- 第6個常量
CONSTANT_Class_info {
 u1 tag = 0x07;
 u2 utf8_index = 0x0016;
}
-- 第7個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0006; => 6個字節長度
 u1 bytes = 0x3c696e69743e
}
-- 第8個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0003; => 3個字節長度
 u1 bytes = 0x282956 
}
-- 第9個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0004; => 4個字節長度
 u1 bytes = 0x436f6465 
}
-- 第10個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x000f; => 15個字節長度
 u1 bytes = 0x4c696e654e756d6265725461626c65 
}
-- 第11個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0004; => 4個字節長度
 u1 bytes = 0x6d61696e 
}
-- 第12個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0016 => 22個字節長度
 u1 bytes = 0x285b4c6a6176612f6c616e672f537472696e673b2956 
}
-- 第13個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x000a; => 10個字節長度
 u1 bytes = 0x536f7572636546696c65 
} 
-- 第14個常量
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0009; => 9個字節長度
 u1 bytes = 0x546573742e6a617661
} 
-- 第15個常量
CONSTANT_NameAndType_info {
 u1 tag = 0x0c;
 u2 class_info_index = 0x0007;
 u1 filed_info_index = 0x0008 
}
-- 第16個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x000c; => 12個字節長度
 u1 bytes = 0x48656c6c6f20576f6c72642e
} 
-- 第17個常量 
CONSTANT_Class_info {
 u1 tag = 0x07;
 u2 index = 0x0017; 
}
-- 第18個常量
CONSTANT_NameAndType_info {
 u1 tag = 0x0c;
 u2 class_info_index = 0x0018; 
 u1 filed_info_index = 0x0019 
}
-- 第19個常量 
CONSTANT_Class_info {
 u1 tag = 0x07;
 u2 index = 0x001a; 
}
-- 第20個常量
CONSTANT_NameAndType_info {
 u1 tag = 0x0c;
 u2 class_info_index = 0x001b; 
 u1 filed_info_index = 0x001c; 
}
-- 第21個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0004; => 4個字節長度
 u1 bytes = 0x54657374 
}
-- 第22個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0010; => 10個字節長度
 u1 bytes = 0x6a6176612f6c616e672f4f626a656374 
} 
-- 第23個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0010; => 10個字節長度
 u1 bytes = 0x6a6176612f6c616e672f53797374656d 
}
-- 第24個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0003; => 3個字節長度
 u1 bytes = 0x6f7574 
}
-- 第25個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0015; => 15個字節長度
 u1 bytes = 0x4c6a6176612f696f2f5072696e7453747265616d3b 
} 
-- 第26個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0013; => 13個字節長度
 u1 bytes = 0x6a6176612f696f2f5072696e7453747265616d 
}
-- 第27個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0007; => 7個字節長度
 u1 bytes = 0x7072696e746c6e 
} 
-- 第28個常量 
CONSTANT_Utf8_info {
 u1 tag = 0x01;
 u2 lenght = 0x0015; => 15個字節長度
 u1 bytes = 0x284c6a6176612f6c616e672f537472696e673b2956 
} 

2.2.2 常量具體的值

根據上面將每個常量對應的信息都標記出來後,可以很方便的按照對應的index進行組裝,結果如下:

第1個常量:java/lang/Object\()V 
第2個常量為:Hello Wolrd.
第3個常量為:java/lang/System out:Ljava/io/PrintStream;
第4個常量為:java/io/PrintStream println(Ljava/lang/String;)V
第5個常量為:Test
第6個常量為:java/lang/Object
第7個常量為:
第8個常量為:()V
第9個常量為:Code
第10個常量為:LineNumberTable
第11個常量為:main
第12個常量為:([Ljava/lang/String;)V
第13個常量為:SourceFile
第14個常量為:Test.java
第15個常量為:()V
第16個常量為:Hello Wolrd.
第17個常量為:java/lang/System
第18個常量為:out:Ljava/io/PrintStream;
第19個常量為:java/io/PrintStream
第20個常量為:println(Ljava/lang/String;)V
第21個常量為:Test
第22個常量為:java/lang/Object
第23個常量為:java/lang/System
第24個常量為:out
第25個常量為:Ljava/io/PrintStream;
第26個常量為:java/io/PrintStream
第27個常量為:println
第28個常量為:(Ljava/lang/String;)V

通過javap來反編譯,查看一下常量池具體信息:

Java 字節碼文件結構

2.3 訪問標誌

在常量池結束之後,緊跟著的2個字節表示訪問標識符:0x0021 訪問標識符表格如下:

Java 字節碼文件結構

由上面的表格可以看到,並沒有0x0021的訪問權限。是因為這裡的訪問標誌可以有多個標誌名稱組成。代碼中的訪問標誌是通過多個值進行或運算得到的結果

2.4 類索引、父類索引、接口索引

在訪問標記後,則是類索引、父類索引、接口索引的數據,這裡數據為:0x0005 0006 0000 。 類索引和父類索引都是一個u2類型的數據,而接口索引集合是一組u2類型的數據的集合,Class 文件中由這三項數據來確定這個類的繼承關係

2.4.1 類索引

類索引在常量池中的索引值為0x0005,十進制為5,指向一個CONSTANT_Class_info類型的常量,其tag值為7,name_index指向21的索引。

Java 字節碼文件結構

2.4.2 父類索引

類索引之後,是父類索引。在常量池中的索引值為0x0006,十進制為6,指向一個CONSTANT_Class_info類型的常量,其tag值為7,name_index指向22的索引。

Java 字節碼文件結構

2.4.3 接口索引

接口索引集合就用來描述哪個類實現了哪些接口,這些被實現的接口將按 implements 語句後的接口順序從左到右排列在接口索引集合中。對於接口索引集合,入口第一項是 u2 類型的數據為接口計數器(interfaces_count),表示索引表的容量,而在接口計數器後則緊跟著所有的接口信息。如果該類沒有實現任何接口,則該計數器值為0,後面接口的索引表不再佔用任何字節

Test類並沒有實現任何接口,所以緊跟在父類索引後的兩個自己是0x0000。這表示該類沒有實現任何接口。因此後面的接口索引表為空

2.5 字段表集合

在接口索引表之後是字段索引計數器和字段索引表。字段索引計數器是一個u2類型的數值,class文件中的值為0x0000,表示有沒有聲明的變量。

字段表中的每項都表示指向常量池中的一個索引,該索引指向一個field_info結構的數據。字段表描述當前類或接口聲明的所有字段,但不包括從父類或接口中繼承過來的。

field_info的結構如下:

Java 字節碼文件結構

2.6 方法表集合

在字段表後的 2 個字節是一個方法計數器,標識當前類中一共有幾個方法,在方法計數器之後,標識的為方法的詳細數據。通過上面的字節碼文件可以看到: 0x0002 表示有兩個方法。方法表的信息如下:

Java 字節碼文件結構

通過方法表和字節碼文件,對應的兩個方法為。第一個方法如下:

0001 0007 0008 0001 0009 0000 001d

方法開始的前2個字節表示訪問標識,這裡是0x0001 。表示public。

緊接著是方法名索引,0x0007指向常量池中的第7個常量,根據上面的內容可以知道為: 。緊著是方法描述符索引。這裡是0x0008,指向常量池中第8個常量,為:()V。

緊接著兩個表示屬性計數器,這裡是0x0001。表示該方法一共有一個屬性,屬性表內容為:

Java 字節碼文件結構

前兩個字節是名字索引,緊接著4個字節是屬性長度,後面是屬性的值。屬性名稱為0x0009。查詢上面的內容可以得到為Code。說明此屬性是方法的字節碼描述。Code屬性的表結構如下:

Java 字節碼文件結構

從這裡開始可以看到,屬性名稱索引對應的為0x0009 ,也就是Code信息。緊接著4個字節長度表示屬性長度。對應的0x0000001d。對應的值為29

屬性長度後是棧深度,2個字節表示。為0x0001

棧深度後是局部變量所需存儲空間,2個字節表示,為: 0x0001。表示局部變量表所需的存儲空間為 1 個 Slot。在這裡 max_locals的單位是Slot,Slot是虛擬機為局部變量分配內存所使用的最小單位

之後是字節碼的長度,為0x00000005。4個字節表示。這裡得出之後的5個字節表示具體的字節碼數據。具體的值為: 2ab7 0001 b1。

具體的字節碼指令表為:

2a -> 查表可知為aload_0。表示將第一個本地變量推送到棧頂
b7 -> invokespecial 調用超類構建方法, 實例初始化方法, 私有方法。這個方法有一個u2類型的參數說明具體調用哪一個方法,它指向常量池中的一個CONSTANT_Methodref_info類型常量,即此方法的方法符號引用
00 -> nop 
01 -> 將null推送至棧頂
b1 -> return 含義是返回此方法,並且返回值為void。這條指令執行後,當前方法結束

異常表的長度為緊跟著的2個字節,這裡為0x0000。也就是沒有對應的異常表數據。

異常表的數據之後是屬性表的信息,這個屬性表的數據是:0x0001。表示有一個屬性信息。屬性表的結構如下

Java 字節碼文件結構

前兩個字節表示屬性的名稱, 這裡值為0x000a。對應的為常量池中第10個常量。具體的為:LineNumberTable。這裡LineNumberTable表結構的數據如下:

Java 字節碼文件結構

緊跟著的4個字節為屬性數據的長度。這個為0x00000006。表示有6個字節的數據。緊接著的0x0001,表示長度為1。後面跟著的是line_number_table類型的數據。line_number_table暫用的分別是4個字節。前兩個字節表示字節碼行號,後兩個自己為源碼行號。可以通過字節碼可以看到為 0x00000001。表示字節碼第0行和源碼第一行。

第一個方法就分析完了。對應的第一個方法的數據為:0001 0007 0008 0001 0009 0000 001d 0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0a00 0000 0600 0100 0000 01

反編譯源碼查看分析結果:

Java 字節碼文件結構

第二個方法的解析類似

00 09 => 訪問權限,這裡是public static void
00 0b => 方法名索引,常量表中第11個常量,為main
00 0c => 方法描述符索引,常量表中第12個常量,為([Ljava/lang/String;)V
00 01 => 屬性表計數器,表示有1個屬性
00 09 => 屬性名稱索引,這裡為Code
00 0000 2b => 表示屬性長度為43
00 02 => 棧深度為2
00 02 => 局部變量存儲的空為2個Slot
00 0000 0b => 字節碼長度為11
12 024c b200 032b b600 04b1 => 具體的字節碼指令
0000 => 異常表長度為0
0001 => 屬性表長度為1
000a => 表示lineNumberTable
0000 000e => 屬性長度為14
0003 => line_number_table 為3
0000 0003=> 源碼第3行對應字節碼對0行
0003 0004 => 源碼第4行對應字節碼第3行
000a 0005 => 源碼第5行對應字節碼第10行
 

第二個方法對應的數據為

00 0900 0b00 0c00 0100 0900 0000 2b00
0200 0200 0000 0b12 024c b200 032b b600
04b1 0000 0001 000a 0000 000e 0003 0000
0003 0003 0004 000a 0005

javap反編譯結果截圖:

Java 字節碼文件結構

2.7 屬性表集合

這裡的屬性表集合是對應的勒種屬性。緊接著下面的數據為

0001 000d 0000 0002 000e

詳細的分析結果如下

0001 => 表示有1個屬性
000d => 為13,表示對應的為常量池中第13個常量。值為SourceFile
0000 0002 => 屬相長度佔用4個字節。表示屬性長度為2
000e => 佔用2個字節,長度為上面0000 0002的值。 對應常量池中第14個常量,為Test.java

屬性截圖為:

Java 字節碼文件結構

3 結果彙總

從整個分析過程來說,其實還是蠻簡單的,不過要認真堅持一步一步分析下來。分析完以後對字節碼文件也就有了初步的認識。一般也不會通過手動一步一步的分析。java提供了javap命令來支持。下面貼出javap編譯的結果

Java 字節碼文件結構

字節碼結果文件備註

魔數:
cafe babe
版本:
0000 0034
常量個數:
001d 
常量池:
0a00 0600 0f08
0010 0900 1100 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 0009 5465 7374 2e6a
6176 610c 0007 0008 0100 0c48 656c 6c6f
2057 6f6c 7264 2e07 0017 0c00 1800 1907
001a 0c00 1b00 1c01 0004 5465 7374 0100
106a 6176 612f 6c61 6e67 2f4f 626a 6563
7401 0010 6a61 7661 2f6c 616e 672f 5379
7374 656d 0100 036f 7574 0100 154c 6a61
7661 2f69 6f2f 5072 696e 7453 7472 6561
6d3b 0100 136a 6176 612f 696f 2f50 7269
6e74 5374 7265 616d 0100 0770 7269 6e74
6c6e 0100 1528 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 
訪問標誌:
0021 
類索引,父類索引,接口索引:
0005 0006 0000
 
字段表集合:
0000 
方法表集合:
0002 0001 0007 0008 0001 0009
0000 001d 0001 0001 0000 0005 2ab7 0001
b100 0000 0100 0a00 0000 0600 0100 0000
0100 0900 0b00 0c00 0100 0900 0000 2b00
0200 0200 0000 0b12 024c b200 032b b600
04b1 0000 0001 000a 0000 000e 0003 0000
0003 0003 0004 000a 0005 
屬性表集合
0001 000d 0000
0002 000e

4 字節碼指令表

Java 字節碼文件結構


分享到:


相關文章: