大家好,我是學(xué)富一車的沉默王二,哈哈哈。(有票圈的讀者應(yīng)該知道這個(gè)梗)
今天我拿了一把小刀,準(zhǔn)備解剖一下 Java 的 class 文件。
CS 的世界里流行著這么一句話,“計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個(gè)中間層來解決”。對(duì)于 Java 來說,JVM 就是這么一個(gè)產(chǎn)物,“Write once, Run anywhere”之所以能實(shí)現(xiàn),靠得就是 JVM,它能在不同的操作系統(tǒng)下運(yùn)行同一份源代碼編譯后的 class 文件。
Java 是跨平臺(tái)的,JVM 作為中間層,自然要針對(duì)不同的操作系統(tǒng)提供不同的實(shí)現(xiàn)。拿 JDK 11 來說,它的實(shí)現(xiàn)就有上圖中提到的這么多種。
通過不同操作系統(tǒng)的 JVM,我們的源代碼就可以不用根據(jù)不同的操作系統(tǒng)編譯成不同的二進(jìn)制可執(zhí)行文件了,跨平臺(tái)的目標(biāo)也就實(shí)現(xiàn)了。那這個(gè) class 文件到底是什么玩意呢?它是怎么被 JVM 識(shí)別的呢?
我們用 IDEA 編寫一段簡單的 Java 代碼,文件名為 Hello.java。
package com.itwanger.jvm;
class Hello {
public static void main(String[] args) {
System.out.println("Hello!");
}
}
點(diǎn)擊編譯按鈕后,IDEA 會(huì)幫我們自動(dòng)生成一個(gè)名為 Hello.class 的文件,在 target/classes
的對(duì)應(yīng)包目錄下。直接雙擊打開后長下面這樣子:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.itwanger.jvm;
class Hello {
Hello() {
}
public static void main(String[] args) {
System.out.println("Hello!");
}
}
看起來和源代碼很像,只是多了一個(gè)空的構(gòu)造方法,對(duì)吧?它是 class 文件被 IDEA 自帶的反編譯工具 Fernflower 反編譯后的樣子。那真實(shí)的 class 文件長什么樣子呢?
可以在 terminal 面板下用 xxd Hello.class
命令來查看。
咦?完全看不懂的樣子呢。它是 class 文件的一種十六進(jìn)制形式,xxd
這個(gè)命令的神奇之處就是它能將一個(gè)給定文件轉(zhuǎn)換成十六進(jìn)制形式。
01、魔數(shù)
第一行中有一串特殊的字符 cafebabe
,它就是一個(gè)魔數(shù),是 JVM 識(shí)別 class 文件的標(biāo)志,JVM 會(huì)在驗(yàn)證階段檢查 class 文件是否以該魔數(shù)開頭,如果不是則會(huì)拋出 ClassFormatError
。
魔數(shù) cafebabe
的中文意思顯而易見,咖啡寶貝,再加上 Java 的圖標(biāo)本來就是一個(gè)熱氣騰騰的咖啡,可見 Java 與咖啡的淵源有多深。
02、版本號(hào)
緊跟著魔數(shù)后面的四個(gè)字節(jié) 0000 0037
分別表示副版本號(hào)和主版本號(hào)。也就是說,主版本號(hào)為 55(0x37 的十進(jìn)制),也就是 Java 11 對(duì)應(yīng)的版本號(hào),副版本號(hào)為 0。
上一個(gè) LTS 版本是 Java 8,對(duì)應(yīng)的主版本號(hào)為 52,也就是說 Java 9 是 53,Java 10 是 54,只不過 Java 9 和 Java 10 都是過渡版本,下一個(gè) LTS 版本是 Java 17,預(yù)計(jì) 2021 年 9 月份推出。
03、常量池
緊跟在版本號(hào)之后的是常量池,字符串常量和較大的證書都會(huì)存儲(chǔ)在常量池中,當(dāng)使用這些數(shù)值時(shí),會(huì)根據(jù)常量池中的索引來查找。
Java 定義了 boolean、byte、short、char 和 int 等基本數(shù)據(jù)類型,它們?cè)诔A砍刂卸紩?huì)被當(dāng)做 int 來處理。我們來通過一段簡單的 Java 代碼了解下。
public class ConstantTest {
public final boolean bool = true;
public final char aChar = 'a';
public final byte b = 66;
public final short s = 67;
public final int i = 68;
}
布爾值 true 的十六進(jìn)制是 0x01、字符 a 的十六進(jìn)制是 0x61,字節(jié) 66 的十六進(jìn)制是 0x42,短整型 67 的十六進(jìn)制是 0x43,整形 68 的十六進(jìn)制是 0x44。所以編譯生成的整形常量在 class 文件中的位置如下圖所示。
第一個(gè)字節(jié) 0x03 表示常量的類型為 CONSTANT_Integer_info,是 JVM 中定義的 14 種常量類型之一,對(duì)應(yīng)的還有 CONSTANT_Float_info、CONSTANT_Long_info、CONSTANT_Double_info,對(duì)應(yīng)的標(biāo)識(shí)分別是 0x04、0x05、0x06。
對(duì)于 int 和 float 來說,它們占 4 個(gè)字節(jié);對(duì)于 long 和 double 來說,它們占 8 個(gè)字節(jié)。來個(gè) long 型的最大值觀察下。
public class ConstantTest {
public final long ong = Long.MAX_VALUE;
}
來看一下它在 class 文件中的位置。05 開頭,7f ff ff ff ff ff ff ff 結(jié)尾,果然占 8 個(gè)字節(jié),以前知道 long 型會(huì)占 8 個(gè)字節(jié),但沒有直觀的感受,現(xiàn)在有了。
接下來,我們?cè)賮砜匆欢未a。
class Hello {
public final String s = "hello";
}
“hello”是一個(gè)字符串,它的十六進(jìn)制為 68 65 6c 6c 6f
,我們來看一下它在 class 文件中的位置。
前面還有 3 個(gè)字節(jié),第一個(gè)字節(jié) 0x01 是標(biāo)識(shí),標(biāo)識(shí)類型為 CONSTANT_Uft8_info,第二個(gè)和第三個(gè)自己 0x00 0x05 用來表示第三部分字節(jié)數(shù)組的長度。
與 CONSTANT_Uft8_info 類型對(duì)應(yīng)的,還有一個(gè) CONSTANT_String_info,用來表示字符串對(duì)象(之前代碼中的 s),標(biāo)識(shí)是 0x08。前者存儲(chǔ)了字符串真正的值,后者并不包含字符串的內(nèi)容,僅僅包含了一個(gè)指向常量池中 CONSTANT_Uft8_info 的索引。來看一下它在 class 文件中的位置。
CONSTANT_String_info 通過索引 19 來找到 CONSTANT_Uft8_info。
除此之外,還有 CONSTANT_Class_info,用來表示類和接口,結(jié)構(gòu)和 CONSTANT_String_info 類似,第一個(gè)字節(jié)是標(biāo)識(shí),值為 0x07,后面兩個(gè)字節(jié)是常量池索引,指向 CONSTANT_Utf8_info——字符串存儲(chǔ)的是類或者接口的全路徑限定名。
拿 Hello.java 類來說,它的全路徑限定名為 com/itwanger/jvm/Hello
,對(duì)應(yīng)的十六進(jìn)制為“636f6d2f697477616e6765722f6a766d2f48656c6c6f”,是一串 CONSTANT_Uft8_info,指向它的 CONSTANT_Class_info 在 class 文件中的什么位置呢?
先不著急,這里給大家介紹一款可視化字節(jié)碼的工具 jclasslib bytecode viewer,可以直接在 IDEA 的插件市場(chǎng)安裝。安裝完成后,選中 class 文件,然后在 View 菜單里找到 Show Bytecode With Jclasslib 子菜單,就可以查看 class 文件的關(guān)鍵信息了。
從上圖中可以看到,常量池的總大小為 23,索引為 04 的 CONSTANT_Class_info 指向的是是索引為 21 的 CONSTANT_Uft8_info,值為 com/itwanger/jvm/Hello
。21 的十六進(jìn)制為 0x15,有了這個(gè)信息,我們就可以找到 CONSTANT_Class_info 在 class 文件中的位置了。
0x07 是第一個(gè)字節(jié),CONSTANT_Class_info 的標(biāo)識(shí)符,然后是兩個(gè)字節(jié),標(biāo)識(shí)索引。
還有 CONSTANT_NameAndType_info,用來標(biāo)識(shí)字段或方法,標(biāo)識(shí)符為 12,對(duì)應(yīng)的十六進(jìn)制是 0x0c。后面還有 4 個(gè)字節(jié),前兩個(gè)是字段或者方法的索引,后兩個(gè)是字段或方法的描述符,也就是字段或者方法的類型。
來看下面這段代碼。
class Hello {
public void testMethod(int id, String name) {
}
}
用 jclasslib 可以看到 CONSTANT_NameAndType_info 包含的索引有兩個(gè)。
一個(gè)是 4,一個(gè)是 5,可以通過下圖來表示 CONSTANT_NameAndType_info 的構(gòu)成。
對(duì)應(yīng) class 文件中的位置如下圖所示。
接下來是 CONSTANT_Fieldref_info 、CONSTANT_Methodref_info 和 CONSTANT_InterfaceMethodref_info,它們?nèi)齻€(gè)的結(jié)構(gòu)比較類似,可以通過下面的偽代碼來表示。
CONSTANT_*ref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
學(xué)過 C 語言的符號(hào)表(Symbol Table)的話,對(duì)這段偽代碼并不會(huì)陌生。
- tag 為標(biāo)識(shí)符,F(xiàn)ieldref 的為 9,也就是十六進(jìn)制的 0x09;Methodref 的為 10,也就是十六進(jìn)制的 0x0a;InterfaceMethodref 的為 11, 也就是十六進(jìn)制的 0x0b。
- class_index 為 CONSTANT_Class_info 的常量池索引,表示字段 | 方法 | 接口方法所在的類信息。
- name_and_type_index 為 CONSTANT_NameAndType_info 的常量池索引,拿 Fieldref 來說,表示字段名和字段類型;拿 Methodref 來說,表示方法名、方法的參數(shù)和返回值類型;拿 InterfaceMethodref 來說,表示接口方法名、接口方法的參數(shù)和返回值類型。
還有 CONSTANT_MethodHandle_info 、CONSTANT_MethodType_info 和 CONSTANT_InvokeDynamic_info,我就不再一一說明了,大家也可以拿把小刀去試一試。
啊,class 文件中最復(fù)雜的常量池部分就算是解剖完了,真不容易!
04、訪問標(biāo)記
緊跟著常量池之后的區(qū)域就是訪問標(biāo)記(Access flags),這個(gè)標(biāo)記用于識(shí)別類或接口的訪問信息,比如說到底是 class 還是 interface?是 public 嗎?是 abstract 抽象類嗎?是 final 類嗎?等等。總共有 16 個(gè)標(biāo)記位可供使用,但常用的只有其中 7 個(gè)。
來看一個(gè)簡單的枚舉代碼。
public enum Color {
RED,GREEN,BLUE;
}
通過 jclasslib 可以看到訪問標(biāo)記的信息有 0x4031 [public final enum]
。
對(duì)應(yīng) class 文件中的位置如下圖所示。
05、this_class、super_class、interfaces
這三部分用來確定類的繼承關(guān)系,this_class 為當(dāng)前類的索引,super_class 為父類的索引,interfaces 為接口。
來看下面這段簡單的代碼,沒有接口,默認(rèn)繼承 Object 類。
class Hello {
public static void main(String[] args) {
}
}
通過 jclasslib 可以看到類的繼承關(guān)系。
- this_class 指向常量池中索引為 2 的 CONSTANT_Class_info。
- super_class 指向常量池中索引為 3 的 CONSTANT_Class_info。
- 由于沒有接口,所以 interfaces 的信息為空。
對(duì)應(yīng) class 文件中的位置如下圖所示。
06、字段表
一個(gè)類中定義的字段會(huì)被存儲(chǔ)在字段表(fields)中,包括靜態(tài)的和非靜態(tài)的。
來看這樣一段代碼。
public class FieldsTest {
private String name;
}
字段只有一個(gè),修飾符為 private,類型為 String,字段名為 name。可以用下面的偽代碼來表示 field 的結(jié)構(gòu)。
field_info {
u2 access_flag;
u2 name_index;
u2 description_index;
}
- access_flag 為字段的訪問標(biāo)記,比如說是不是 public | private | protected,是不是 static,是不是 final 等。
- name_index 為字段名的索引,指向常量池中的 CONSTANT_Utf8_info, 比如說上例中的值就為 name。
- description_index 為字段的描述類型索引,也指向常量池中的 CONSTANT_Utf8_info,針對(duì)不同的數(shù)據(jù)類型,會(huì)有不同規(guī)則的描述信息。
1)對(duì)于基本數(shù)據(jù)類型來說,使用一個(gè)字符來表示,比如說 I 對(duì)應(yīng)的是 int,B 對(duì)應(yīng)的是 byte。
2)對(duì)于引用數(shù)據(jù)類型來說,使用 L***;
的方式來表示,L
開頭,;
結(jié)束,比如字符串類型為 Ljava/lang/String;
。
3)對(duì)于數(shù)組來說,會(huì)用一個(gè)前置的 [
來表示,比如說字符串?dāng)?shù)組為 [Ljava/lang/String;
。
對(duì)應(yīng)到 class 文件中的位置如下圖所示。
07、方法表
方法表和字段表類似,區(qū)別是用來存儲(chǔ)方法的信息,包括方法名,方法的參數(shù),方法的簽名。
就拿 main 方法來說吧。
public class MethodsTest {
public static void main(String[] args) {
}
}
先用 jclasslib 看一下大概的信息。
- 訪問標(biāo)記是 public static 的。
- 方法名為 main。
- 方法的參數(shù)為字符串?dāng)?shù)組;返回類型為 Void。
對(duì)應(yīng)到 class 文件中的位置如下圖所示。
08、屬性表
屬性表是 class 文件中的最后一部分,通常出現(xiàn)在字段和方法中。
來看這樣一段代碼。
public class AttributeTest {
public static final int DEFAULT_SIZE = 128;
}
只有一個(gè)常量 DEFAULT_SIZE,它屬于字段中的一種,就是加了 final 的靜態(tài)變量。先通過 jclasslib 看一下它當(dāng)中一個(gè)很重要的屬性——ConstantValue,用來表示靜態(tài)變量的初始值。
- Attribute name index 指向常量池中值為“ConstantValue”的常量。
- Attribute length 的值為固定的 2,因?yàn)樗饕徽純蓚€(gè)字節(jié)的大小。
- Constant value index 指向常量池中具體的常量,如果常量類型為 int,指向的就是 CONSTANT_Integer_info。
我畫了一副圖,可以完整的表示字段的結(jié)構(gòu),包含屬性表在內(nèi)。
對(duì)應(yīng)到 class 文件中的位置如下圖所示。
來看下面這段代碼。
public class MethodCode {
public static void main(String[] args) {
foo();
}
private static void foo() {
}
}
main 方法中調(diào)用了 foo 方法。通過 jclasslib 看一下它當(dāng)中一個(gè)很重要的屬性——Code, 方法的關(guān)鍵信息都存儲(chǔ)在里面。
- Attribute name index 指向常量池中值為“Code”的常量。
- Attribute length 為屬性值的長度大小。
- bytecode 存儲(chǔ)真正的字節(jié)碼指令。
- exception table 表示方法內(nèi)部的異常信息。
- maximum stack size 表示操作數(shù)棧的最大深度,方法執(zhí)行的任意期間操作數(shù)棧深度都不會(huì)超過這個(gè)值。
- maximum local variable 表示臨時(shí)變量表的大小,注意,并不等于方法中所有臨時(shí)變量的數(shù)量之和,當(dāng)一個(gè)作用域結(jié)束,內(nèi)部的臨時(shí)變量占用的位置就會(huì)被替換掉。
- code length 表示字節(jié)碼指令的長度。
對(duì)應(yīng) class 文件中的位置如下圖所示。
到此為止,class 文件的內(nèi)部算是剖析得差不多了,希望能對(duì)大家有所幫助。第一次拿刀,手有點(diǎn)顫,如果哪里有不足的地方,歡迎大家在評(píng)論區(qū)毫不留情地指出來!
超級(jí)硬核,解剖了超長時(shí)間,累壞了,記得幫我點(diǎn)贊鼓勵(lì)下吧~