一把小刀,直插 class 文件的小心臟

大家好,我是學(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_infoCONSTANT_Long_infoCONSTANT_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_infoCONSTANT_Methodref_infoCONSTANT_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_infoCONSTANT_MethodType_infoCONSTANT_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ì)下吧~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評(píng)論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容