轉載請註明原創出處,謝謝!
談到類文件結構,就要從 Java 虛擬機說起。
一、Java 虛擬機是一個與語言無關的平臺
實現語言無關性的基礎是虛擬機和字節碼存儲格式。 Java 虛擬機不與任何語言綁定,它只與 “Class 文件” 這種特定的二進制文件格式所關聯。
任意一門語言都可以按照 Java 虛擬機規範把程序代碼編譯成 Class 文件,然後在虛擬機上運行。例如:使用 Java 編譯器可以把 Java 代碼編譯成存儲字節碼的 Class 文件,使用 JRuby 等其他語言的編譯器一樣可以把程序代碼編譯成 Class 文件,虛擬機並不關心 Class 的來源是何種語言,如下圖所示:
二、什麼是 Class 類文件結構?
任意一個有效的類或接口所應當滿足的格式稱爲 “Class 文件格式”,實際上它不一定以磁盤文件的形式存在。
Class 文件是一組以 8 位字節爲基礎單位的二進制流,各項數據項目嚴格按照順序緊湊地排列在 Class 文件之中。當遇到需要佔用 8 位字節以上空間的數據項時,則會按照高位在前(Big–Endian)的方式分割成若干個 8 位字節進行存儲。
"Class 文件"說白了就是程序代碼編碼解碼的中間結果。將程序代碼編譯成 Class 文件的過程就是編碼,而虛擬機加載 Class 文件的過程就是解碼的過程,編碼和解碼需要嚴格遵守虛擬機規範。
下面開始詳細的介紹 Class 文件格式(這個過程比較枯燥,但這是瞭解虛擬機的重要基礎之一):
根據 Java 虛擬機規範的規定,Class 文件格式採用一種類似於 C 語言結構體的僞結構來存儲數據,這種僞結構只有兩種數據類型:無符號數和表。所以這裏先介紹無符號數和表這兩個概念:
(1)無符號數
- 無符號數屬於基本的數據類型,以
u1、u2、u4、u8
來分別表示 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照 UTF-8 編碼構成字符串值。
(2)表
- 表是有多個無符號數或者其他表作爲數據項構成的符合數據類型,所有表都習慣性地以 “_info” 結尾。表用於描述有層次關係的複合結構的數據。
Class 文件本質上就是一張表:
由於表中存在一些不定項,所以採取了 “size + size 個數據項” 的形式來描述,如上圖中的 fields_count 表示 field_info 的個數,其後跟了 fields_count 個 field_info。
注:由於這個地方是用 u2
即 2 個字節的無符號數來表示個數,那麼,假如一個類的 field 的個數超過了 2 個字節所能表示的最大值,則該類會編譯失敗,不過這種情況基本上不可能出現。
接下來逐個理解上表中各個數據項的具體含義:
我們以最簡單的一個 Java 代碼爲例:
package com.yhh.example;
public class TestClass {
private int index;
public Integer inc() {
return index++;
}
}
首先通過 javac TestClass.java
得到編譯後的 TestClass.class 文件。
然後通過 hexdump -C ClassName.class
命令查看文件 TestClass.class 的內容(稍後會以這個爲基礎進行舉例):
➜ example git:(master) ✗ hexdump -C TestClass.class
00000000 ca fe ba be 00 00 00 34 00 19 0a 00 05 00 10 09 |.......4........|
00000010 00 04 00 11 0a 00 12 00 13 07 00 14 07 00 15 01 |................|
00000020 00 05 69 6e 64 65 78 01 00 01 49 01 00 06 3c 69 |..index...I...<i|
00000030 6e 69 74 3e 01 00 03 28 29 56 01 00 04 43 6f 64 |nit>...()V...Cod|
00000040 65 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 |e...LineNumberTa|
00000050 62 6c 65 01 00 03 69 6e 63 01 00 15 28 29 4c 6a |ble...inc...()Lj|
00000060 61 76 61 2f 6c 61 6e 67 2f 49 6e 74 65 67 65 72 |ava/lang/Integer|
00000070 3b 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 |;...SourceFile..|
00000080 0e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 0c |.TestClass.java.|
00000090 00 08 00 09 0c 00 06 00 07 07 00 16 0c 00 17 00 |................|
000000a0 18 01 00 19 63 6f 6d 2f 79 68 68 2f 65 78 61 6d |....com/yhh/exam|
000000b0 70 6c 65 2f 54 65 73 74 43 6c 61 73 73 01 00 10 |ple/TestClass...|
000000c0 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 |java/lang/Object|
000000d0 01 00 11 6a 61 76 61 2f 6c 61 6e 67 2f 49 6e 74 |...java/lang/Int|
000000e0 65 67 65 72 01 00 07 76 61 6c 75 65 4f 66 01 00 |eger...valueOf..|
000000f0 16 28 49 29 4c 6a 61 76 61 2f 6c 61 6e 67 2f 49 |.(I)Ljava/lang/I|
00000100 6e 74 65 67 65 72 3b 00 21 00 04 00 05 00 00 00 |nteger;.!.......|
00000110 01 00 02 00 06 00 07 00 00 00 02 00 01 00 08 00 |................|
00000120 09 00 01 00 0a 00 00 00 1d 00 01 00 01 00 00 00 |................|
00000130 05 2a b7 00 01 b1 00 00 00 01 00 0b 00 00 00 06 |.*..............|
00000140 00 01 00 00 00 03 00 01 00 0c 00 0d 00 01 00 0a |................|
00000150 00 00 00 27 00 04 00 01 00 00 00 0f 2a 59 b4 00 |...'........*Y..|
00000160 02 5a 04 60 b5 00 02 b8 00 03 b0 00 00 00 01 00 |.Z.`............|
00000170 0b 00 00 00 06 00 01 00 00 00 07 00 01 00 0e 00 |................|
00000180 00 00 02 00 0f |.....|
00000185
1、魔數和 Class 文件的版本
每個 Class 文件的頭 4 個字節稱爲魔數(Magic Number),它的唯一作用是確定這個文件是否爲一個能被虛擬機接受的 Class 文件。使用魔數而不是拓展名來進行識別主要是基於安全方面的考慮,因爲文件拓展名可以隨意更改。
魔數的值爲 0xCAFEBABE
;
緊接着魔數的 4 個字節是 Class 文件的版本號:第 5 和第 6 個字節是次版本號(Minor Version),第 7 和第 8 個字節是主版本號(Major Version)。Java 的版本號是從 45 開始的,JDK 1.1 之後的每個 JDK 大版本發佈主版本號向上加 1(JDK 1.0 ~ 1.1 使用了 45.0 ~ 45.3 的版本號),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能運行以後版本的 Class 文件,即使文件格式並未發生任何變化,虛擬機也必須拒絕執行超過其版本號的 Class 文件。
以 TestClass.class 爲例:
00000000 ca fe ba be 00 00 00 34
可以看到,頭 4 個字節爲 0xcafebabe
,然後是次版本號 0x0000
,而主版本號爲 0x0034
,即十進制的52,即 Class 文件的十進制版本號爲:52.0,說明該文件是使用 JDK 1.8 編譯的 Class 文件。
2、常量池
緊接着主次版本號之後的是常量池入口,常量池可以理解爲 Class 文件之中的資源倉庫,它是 Class 文件結構中與其他項目關聯最多的數據類型,也是佔用 Class 文件空間最大的數據項目之一。
由於常量池中常量的數量是不固定的,所以需要有一個 u2
類型的數據來表示常量池容量計數值(constant_pool_count)。這裏需要注意的是,常量池容量計數值是從 1 而不是 0 開始的,這是因爲第 0 項常量空出來是有特殊考慮的,這樣做的目的在於滿足在特定情況下表達 “不引用任何一個常量池項目” 的含義,這種情況可以把索引值置爲 0 來表示。 Class 文件結構中只有常量池的容量計數是從 1 開始,對於其他集合類型,都是從 0 開始的。
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)。字面量比較接近於 Java 語言層面的常量概念,如文本字符串、聲明爲 final 的常量值等。而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:
1)類和接口的全限定名
2)字段的名稱和描述符
3)方法的名稱和描述符
常量池中每一項常量都是一個表,它們有一個共同點:就是表開始的第一位是一個 u2
類型的標誌位,代表當前這個常量屬於哪種類型。常量類型所代表的具體含義見下圖:
CONSTANT_Class_info 的結構比較簡單,見下圖:
tag 是標誌位,代表它是屬於哪種常量類型;name_index 是一個索引值,它指向常量池中一個 CONSTANT_Utf8_info 類型常量。
CONSTANT_Utf8_info 型常量的結構如下:
tag 同上;length 代表這個 UTF-8 編碼的字符串長度是多少字節,後面緊跟着的長度爲 length 字節的連續數據是一個使用 UTF-8 縮略編碼表示的字符串。
由於 Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量來描述名稱,所以 CONSTANT_Utf8_info 型常量的最大長度也就是 Java 中方法、字段名的最大長度。如果 Java 程序中如果定義了超過這個最大長度的變量名或方法名,將會無法編譯。
下圖是常量池中所有常量項的結構總表:
以 TestClass.class 爲例:
00 19 0a 00 05 00 10 09 |.......4........|
00000010 00 04 00 11 0a 00 12 00 13 07 00 14 07 00 15 01 |................|
00000020 00 05 69 6e 64 65 78 01 00 01 49 01 00 06 3c 69 |..index...I...<i|
00000030 6e 69 74 3e 01 00 03 28 29 56 01 00 04 43 6f 64 |nit>...()V...Cod|
00000040 65 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 |e...LineNumberTa|
00000050 62 6c 65 01 00 03 69 6e 63 01 00 15 28 29 4c 6a |ble...inc...()Lj|
00000060 61 76 61 2f 6c 61 6e 67 2f 49 6e 74 65 67 65 72 |ava/lang/Integer|
00000070 3b 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 |;...SourceFile..|
00000080 0e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 0c |.TestClass.java.|
00000090 00 08 00 09 0c 00 06 00 07 07 00 16 0c 00 17 00 |................|
000000a0 18 01 00 19 63 6f 6d 2f 79 68 68 2f 65 78 61 6d |....com/yhh/exam|
000000b0 70 6c 65 2f 54 65 73 74 43 6c 61 73 73 01 00 10 |ple/TestClass...|
000000c0 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 |java/lang/Object|
000000d0 01 00 11 6a 61 76 61 2f 6c 61 6e 67 2f 49 6e 74 |...java/lang/Int|
000000e0 65 67 65 72 01 00 07 76 61 6c 75 65 4f 66 01 00 |eger...valueOf..|
000000f0 16 28 49 29 4c 6a 61 76 61 2f 6c 61 6e 67 2f 49 |.(I)Ljava/lang/I|
00000100 6e 74 65 67 65 72 3b
第一項 u2
類型 0x0019
爲常量池的容量,即十進制的 25 ,代表一共有 24(第 0 項空出來有特殊用途)項常量。然後是一位 u1
類型爲 tag, 0x0a
,代表是類中方法的符號引用(CONSTANT_Class_info),然後是 u2
類型,index,0x0005
,表示聲明方法的類描述符的爲常量池第 5 項常量。接下來還是 u2
類型,index,0x0010
,表示名稱即類型描述符爲常量池第 16 項常量。然後第一項常量就結束了,繼續第二個常量。tag, 0x09
,代表類中字段的符號引用(CONSTANT_Fieldref_info),然後 u2
類型,index,0x0004
,表示聲明字段的類或接口描述符爲常量池第 4 項常量。 0x0011
, 表示字段的描述符爲常量池中第 17 項常量,第二項常量結束。然後繼續按照此方法,可以得到剩下的 22 項常量。
3、訪問標誌
在常量池結束之後,緊接着的兩個字節代表訪問標誌(access_flags),這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個 Class 是類還是接口;是否定義爲 public 類型;是否定義爲 abstract 類型;如果是類的話,是否被聲明爲 final 等。具體標誌及含義如下圖:
以 TestClass.class 爲例:
00000108 00 21
通過分析代碼,只有 0x0001
和 0x0020
兩個標誌位爲真,即它的 access_flags 的值應爲 0x0021
,通過前面的計算,可以知道 access_flags 標誌(偏移地址: 0x00000108
)的確爲 0x0021
。
4、類索引、父類索引和接口索引集合
類索引(this_class)和父類索引(super_class)都是一個 u2
類型的數據,而接口索引集合(interfaces)是一組 u2
類型的數據的集合,Class 文件通過這三項數據來確定這個類的繼承關係。
類索引、父類索引和接口索引集合都按照順序排列在訪問標誌之後,類索引和父類索引引用兩個 u2
類型的索引值表示,它們各自指向一個類型爲 CONSTANT_Class_info 的類描述符常量,從而得到全限定名字符串。
而接口索引集合,第一項爲接口計數器,表示索引表的容量。如果該類沒有實現任何接口,則該計數器值爲 0。
以 TestClass.class 爲例:
0000010a 00 04 00 05 00 00
第一項 u2
類型 0x0004
,表示該類的全限定名爲常量池中第 4 項常量(com/yhh/example/TestClass),第二項 u2
類型 0x0005
,表示該類的父類全限定名爲常量池中第 5 項常量(java/lang/Object),緊接着一項 u2
類型 0x0000
表示接口索引集合大小爲 0 。
5、字段表集合
字段表(field_info)用於描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,但不包括在方法內部聲明的局部變量。
字段表結構如下:
字段修飾符放在 access_flags 項目中,可以設置的標誌位及含義如下:
跟隨 access_flags 標誌的是兩個索引值:name_index 和 descriptor_index。它們都是對常量池的引用,分別代表着字段的簡單名稱以及字段和方法的描述符。
關於字段和方法的描述符的標識字符及含義如下:
對於數組類型,每一維度將使用一個前置的 “[” 字符來描述,如一個定義爲 “java.lang.String[][]” 類型的二維數組表示爲:"[[Ljava/lang/String",一個整型數組 “int[]” 將被記錄爲 “[I”。
描述方法時,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號 “()” 之內。如方法 void inc() 的描述符爲 “()V”,方法 java.lang.String toString() 的描述符爲 “()Ljava/lang/String;”。
以 TestClass.class 爲例:
0000010a 00 |nteger;.!.......|
00000110 01 00 02 00 06 00 07 00 00
第一項 u2
類型值爲 0x0001
,代表集合中只有一個方法,第二項 u2
類型爲 access_flags ,值爲 0x0002
,表示方法是 private 的,第三項 u2
類型爲 name_index,值爲 0x0006
,即字段的簡單名稱指向常量池中第六項常量,第三項 u2
類型爲 descriptor_index,值爲 0x0007
,即字段和方法的描述符指向常量池第七項常量。接下來一個 u2
類型值爲 0x0000
,說明沒有需要額外描述的內容。
6、方法表集合
方法表(method_info)結構和字段表結構是一模一樣的,除了訪問標誌和屬性表集合的可選項有所區別。
因爲 volatile 關鍵字和 transient 關鍵字不能修飾方法,所以方法表的訪問標誌中沒有了 ACC_VLOLATILE 標誌和 ACC_TRANSIENT 標誌。與之對應的,新增了 synchronized、native、strictfp 和 abstract 關鍵字的訪問標誌。具體標誌位及其取值見下表:
以 TestClass.class 爲例:
0000011a 00 02 00 01 00 08 00 |................|
00000120 09 00 01 00 0a 00 00 00 1d 00 01 00 01 00 00 00 |................|
00000130 05 2a b7 00 01 b1 00 00 00 01 00 0b 00 00 00 06 |.*..............|
00000140 00 01 00 00 00 03 00 01 00 0c 00 0d 00 01 00 0a |................|
00000150 00 00 00 27 00 04 00 01 00 00 00 0f 2a 59 b4 00 |...'........*Y..|
00000160 02 5a 04 60 b5 00 02 b8 00 03 b0 00 00 00 01 00 |.Z.`............|
00000170 0b 00 00 00 06 00 01 00 00 00 07
方法表和字段表沒有太大區別, 0x0002
代表有兩個方法,第一個方法訪問標誌是 0x0001
,即是 public 的, name_index 爲 0x0008
,指向常量池中第 8 項常量,descriptor_index 爲 0x0009
,指向常量池中第 9 項常量; 下一個 u2
類型爲 attributes_count,值爲 0x0001
,說明有一個屬性值,緊接着是 attribute_name_index 爲 0x000a
,即指向常量池中第 10 項常量,通過查找得到,第 10 項常量爲 Code,下一個 u4
類型爲 attribute_length,值爲 0x0000001d
,即 29 ,下一個 u2
類型爲 max_stack,值爲 0x0001
,表示操作數棧深度的最大值,下一個 u2
類型爲 max_locals,值爲 0x0001
,代表了局部變量表所需的存儲空間;接下來是 code_length 和 code,用來存儲 Java 源程序編譯後生成的字節碼指令,第一個 u4
類型爲 code_length,值爲 0x00000005
,表示字節碼區域共佔 5 個字節,然後讀入緊隨的 5 個字節,並根據字節碼指令表翻譯出對應的字節碼指令。
翻譯 2a b7 00 01 b1
的過程爲:
1)讀入 2a
,查表得 0x2a
對應的指令爲 aload_0,這個指令的含義是將第 0 個 Slot 中爲 reference 類型的本地變量推送到操作數棧頂。
2)讀入 b7
,查表得 0xb7
對應的指令爲 invokespecial,其後有一個 u2
類型的參數說明調用哪一個方法。
3)讀入 0001
,是 invokespecial 指令的參數,指向常量池中第 1 個常量。
4)讀入 b1
,查表得 0xb1
對應的指令爲 return,含義是返回此方法,並且返回值爲 void,方法結束。
後面就都是根據類似的方法推出得到。
7、屬性表集合
在 Class 文件、字段表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的信息。
以 TestClass.class 爲例:
0000017b 00 01 00 0e 00 |................|
00000180 00 00 02 00 0f |.....|
00000185
包含一個 SourceFile 的屬性,sourcefile_index 值爲 0x000f
,指向常量池第 15 個常量。
至此,TestClass.class 文件字節碼分析結束。
最後使用 javap
工具來得到一個完整的 TestClass.class 文件字節碼內容如下:
➜ example git:(master) ✗ javap -verbose TestClass.class
Classfile /Users/yanghaihui/project/github/code/java/src/main/java/com/yhh/example/TestClass.class
Last modified Jun 9, 2018; size 389 bytes
MD5 checksum 510375b08360425cddc8b51216c5155d
Compiled from "TestClass.java"
public class com.yhh.example.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#17 // com/yhh/example/TestClass.index:I
#3 = Methodref #18.#19 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#4 = Class #20 // com/yhh/example/TestClass
#5 = Class #21 // java/lang/Object
#6 = Utf8 index
#7 = Utf8 I
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 inc
#13 = Utf8 ()Ljava/lang/Integer;
#14 = Utf8 SourceFile
#15 = Utf8 TestClass.java
#16 = NameAndType #8:#9 // "<init>":()V
#17 = NameAndType #6:#7 // index:I
#18 = Class #22 // java/lang/Integer
#19 = NameAndType #23:#24 // valueOf:(I)Ljava/lang/Integer;
#20 = Utf8 com/yhh/example/TestClass
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/Integer
#23 = Utf8 valueOf
#24 = Utf8 (I)Ljava/lang/Integer;
{
public com.yhh.example.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public java.lang.Integer inc();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field index:I
5: dup_x1
6: iconst_1
7: iadd
8: putfield #2 // Field index:I
11: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: areturn
LineNumberTable:
line 7: 0
}
SourceFile: "TestClass.java"
參考資料:
(1)《深入理解 Java 虛擬機》周志明 著.