深入理解 JVM(2)類文件結構

轉載請註明原創出處,謝謝!

HappyFeet的博客

談到類文件結構,就要從 Java 虛擬機說起。


一、Java 虛擬機是一個與語言無關的平臺

實現語言無關性的基礎是虛擬機和字節碼存儲格式。 Java 虛擬機不與任何語言綁定,它只與 “Class 文件” 這種特定的二進制文件格式所關聯。

任意一門語言都可以按照 Java 虛擬機規範把程序代碼編譯成 Class 文件,然後在虛擬機上運行。例如:使用 Java 編譯器可以把 Java 代碼編譯成存儲字節碼的 Class 文件,使用 JRuby 等其他語言的編譯器一樣可以把程序代碼編譯成 Class 文件,虛擬機並不關心 Class 的來源是何種語言,如下圖所示:

Java虛擬機提供的語言無關性

二、什麼是 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 文件本質上就是一張表:

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 的結構比較簡單,見下圖:

CONSTANT_Class_info型常量結構1

tag 是標誌位,代表它是屬於哪種常量類型;name_index 是一個索引值,它指向常量池中一個 CONSTANT_Utf8_info 類型常量。

CONSTANT_Utf8_info 型常量的結構如下:

CONSTANT_Utf8_info型常量的結構

tag 同上;length 代表這個 UTF-8 編碼的字符串長度是多少字節,後面緊跟着的長度爲 length 字節的連續數據是一個使用 UTF-8 縮略編碼表示的字符串。

由於 Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量來描述名稱,所以 CONSTANT_Utf8_info 型常量的最大長度也就是 Java 中方法、字段名的最大長度。如果 Java 程序中如果定義了超過這個最大長度的變量名或方法名,將會無法編譯。

下圖是常量池中所有常量項的結構總表:

常量池中所有常量項的結構總表1

常量池中所有常量項的結構總表2

以 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

通過分析代碼,只有 0x00010x0020 兩個標誌位爲真,即它的 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 虛擬機》周志明 著.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章