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

大家好,我是學富一車的沉默王二,哈哈哈。(有票圈的讀者應該知道這個梗)

今天我拿了一把小刀,準備解剖一下 Java 的 class 文件。

CS 的世界里流行著這么一句話,“計算機科學領域的任何問題都可以通過增加一個中間層來解決”。對于 Java 來說,JVM 就是這么一個產物,“Write once, Run anywhere”之所以能實現,靠得就是 JVM,它能在不同的操作系統下運行同一份源代碼編譯后的 class 文件。

Java 是跨平臺的,JVM 作為中間層,自然要針對不同的操作系統提供不同的實現。拿 JDK 11 來說,它的實現就有上圖中提到的這么多種。

通過不同操作系統的 JVM,我們的源代碼就可以不用根據不同的操作系統編譯成不同的二進制可執行文件了,跨平臺的目標也就實現了。那這個 class 文件到底是什么玩意呢?它是怎么被 JVM 識別的呢?

我們用 IDEA 編寫一段簡單的 Java 代碼,文件名為 Hello.java。

package com.itwanger.jvm;
class Hello {
    public static void main(String[] args) {
        System.out.println("Hello!");
    }
}

點擊編譯按鈕后,IDEA 會幫我們自動生成一個名為 Hello.class 的文件,在 target/classes 的對應包目錄下。直接雙擊打開后長下面這樣子:

//
// 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!");
    }
}

看起來和源代碼很像,只是多了一個空的構造方法,對吧?它是 class 文件被 IDEA 自帶的反編譯工具 Fernflower 反編譯后的樣子。那真實的 class 文件長什么樣子呢?

可以在 terminal 面板下用 xxd Hello.class 命令來查看。

咦?完全看不懂的樣子呢。它是 class 文件的一種十六進制形式,xxd 這個命令的神奇之處就是它能將一個給定文件轉換成十六進制形式。

01、魔數

第一行中有一串特殊的字符 cafebabe,它就是一個魔數,是 JVM 識別 class 文件的標志,JVM 會在驗證階段檢查 class 文件是否以該魔數開頭,如果不是則會拋出 ClassFormatError

魔數 cafebabe 的中文意思顯而易見,咖啡寶貝,再加上 Java 的圖標本來就是一個熱氣騰騰的咖啡,可見 Java 與咖啡的淵源有多深。

02、版本號

緊跟著魔數后面的四個字節 0000 0037 分別表示副版本號和主版本號。也就是說,主版本號為 55(0x37 的十進制),也就是 Java 11 對應的版本號,副版本號為 0。

上一個 LTS 版本是 Java 8,對應的主版本號為 52,也就是說 Java 9 是 53,Java 10 是 54,只不過 Java 9 和 Java 10 都是過渡版本,下一個 LTS 版本是 Java 17,預計 2021 年 9 月份推出。

03、常量池

緊跟在版本號之后的是常量池,字符串常量和較大的證書都會存儲在常量池中,當使用這些數值時,會根據常量池中的索引來查找。

Java 定義了 boolean、byte、short、char 和 int 等基本數據類型,它們在常量池中都會被當做 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 的十六進制是 0x01、字符 a 的十六進制是 0x61,字節 66 的十六進制是 0x42,短整型 67 的十六進制是 0x43,整形 68 的十六進制是 0x44。所以編譯生成的整形常量在 class 文件中的位置如下圖所示。

第一個字節 0x03 表示常量的類型為 CONSTANT_Integer_info,是 JVM 中定義的 14 種常量類型之一,對應的還有 CONSTANT_Float_infoCONSTANT_Long_infoCONSTANT_Double_info,對應的標識分別是 0x04、0x05、0x06。

對于 int 和 float 來說,它們占 4 個字節;對于 long 和 double 來說,它們占 8 個字節。來個 long 型的最大值觀察下。

public class ConstantTest {
    public final long ong = Long.MAX_VALUE;
}

來看一下它在 class 文件中的位置。05 開頭,7f ff ff ff ff ff ff ff 結尾,果然占 8 個字節,以前知道 long 型會占 8 個字節,但沒有直觀的感受,現在有了。

接下來,我們再來看一段代碼。

class Hello {
    public final String s = "hello";
}

“hello”是一個字符串,它的十六進制為 68 65 6c 6c 6f,我們來看一下它在 class 文件中的位置。

前面還有 3 個字節,第一個字節 0x01 是標識,標識類型為 CONSTANT_Uft8_info,第二個和第三個自己 0x00 0x05 用來表示第三部分字節數組的長度。

CONSTANT_Uft8_info 類型對應的,還有一個 CONSTANT_String_info,用來表示字符串對象(之前代碼中的 s),標識是 0x08。前者存儲了字符串真正的值,后者并不包含字符串的內容,僅僅包含了一個指向常量池中 CONSTANT_Uft8_info 的索引。來看一下它在 class 文件中的位置。

CONSTANT_String_info 通過索引 19 來找到 CONSTANT_Uft8_info

除此之外,還有 CONSTANT_Class_info,用來表示類和接口,結構和 CONSTANT_String_info 類似,第一個字節是標識,值為 0x07,后面兩個字節是常量池索引,指向 CONSTANT_Utf8_info——字符串存儲的是類或者接口的全路徑限定名。

拿 Hello.java 類來說,它的全路徑限定名為 com/itwanger/jvm/Hello,對應的十六進制為“636f6d2f697477616e6765722f6a766d2f48656c6c6f”,是一串 CONSTANT_Uft8_info,指向它的 CONSTANT_Class_info 在 class 文件中的什么位置呢?

先不著急,這里給大家介紹一款可視化字節碼的工具 jclasslib bytecode viewer,可以直接在 IDEA 的插件市場安裝。安裝完成后,選中 class 文件,然后在 View 菜單里找到 Show Bytecode With Jclasslib 子菜單,就可以查看 class 文件的關鍵信息了。

從上圖中可以看到,常量池的總大小為 23,索引為 04 的 CONSTANT_Class_info 指向的是是索引為 21 的 CONSTANT_Uft8_info,值為 com/itwanger/jvm/Hello。21 的十六進制為 0x15,有了這個信息,我們就可以找到 CONSTANT_Class_info 在 class 文件中的位置了。

0x07 是第一個字節,CONSTANT_Class_info 的標識符,然后是兩個字節,標識索引。

還有 CONSTANT_NameAndType_info,用來標識字段或方法,標識符為 12,對應的十六進制是 0x0c。后面還有 4 個字節,前兩個是字段或者方法的索引,后兩個是字段或方法的描述符,也就是字段或者方法的類型。

來看下面這段代碼。

class Hello {
    public void testMethod(int id, String name) {
    }
}

用 jclasslib 可以看到 CONSTANT_NameAndType_info 包含的索引有兩個。

一個是 4,一個是 5,可以通過下圖來表示 CONSTANT_NameAndType_info 的構成。

對應 class 文件中的位置如下圖所示。

接下來是 CONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info,它們三個的結構比較類似,可以通過下面的偽代碼來表示。

CONSTANT_*ref_info {
  u1 tag;
  u2 class_index;
  u2 name_and_type_index;
}

學過 C 語言的符號表(Symbol Table)的話,對這段偽代碼并不會陌生。

  • tag 為標識符,Fieldref 的為 9,也就是十六進制的 0x09;Methodref 的為 10,也就是十六進制的 0x0a;InterfaceMethodref 的為 11, 也就是十六進制的 0x0b。
  • class_index 為 CONSTANT_Class_info 的常量池索引,表示字段 | 方法 | 接口方法所在的類信息。
  • name_and_type_index 為 CONSTANT_NameAndType_info 的常量池索引,拿 Fieldref 來說,表示字段名和字段類型;拿 Methodref 來說,表示方法名、方法的參數和返回值類型;拿 InterfaceMethodref 來說,表示接口方法名、接口方法的參數和返回值類型。

還有 CONSTANT_MethodHandle_infoCONSTANT_MethodType_infoCONSTANT_InvokeDynamic_info,我就不再一一說明了,大家也可以拿把小刀去試一試。

啊,class 文件中最復雜的常量池部分就算是解剖完了,真不容易!

04、訪問標記

緊跟著常量池之后的區域就是訪問標記(Access flags),這個標記用于識別類或接口的訪問信息,比如說到底是 class 還是 interface?是 public 嗎?是 abstract 抽象類嗎?是 final 類嗎?等等。總共有 16 個標記位可供使用,但常用的只有其中 7 個。

來看一個簡單的枚舉代碼。

public enum Color {
    RED,GREEN,BLUE;
}

通過 jclasslib 可以看到訪問標記的信息有 0x4031 [public final enum]

對應 class 文件中的位置如下圖所示。

05、this_class、super_class、interfaces

這三部分用來確定類的繼承關系,this_class 為當前類的索引,super_class 為父類的索引,interfaces 為接口。

來看下面這段簡單的代碼,沒有接口,默認繼承 Object 類。

class Hello {
    public static void main(String[] args) {
        
    }
}

通過 jclasslib 可以看到類的繼承關系。

  • this_class 指向常量池中索引為 2 的 CONSTANT_Class_info
  • super_class 指向常量池中索引為 3 的 CONSTANT_Class_info
  • 由于沒有接口,所以 interfaces 的信息為空。

對應 class 文件中的位置如下圖所示。

06、字段表

一個類中定義的字段會被存儲在字段表(fields)中,包括靜態的和非靜態的。

來看這樣一段代碼。

public class FieldsTest {
    private String name;
}

字段只有一個,修飾符為 private,類型為 String,字段名為 name。可以用下面的偽代碼來表示 field 的結構。

field_info {
  u2 access_flag;
  u2 name_index;
  u2 description_index;
}
  • access_flag 為字段的訪問標記,比如說是不是 public | private | protected,是不是 static,是不是 final 等。
  • name_index 為字段名的索引,指向常量池中的 CONSTANT_Utf8_info, 比如說上例中的值就為 name。
  • description_index 為字段的描述類型索引,也指向常量池中的 CONSTANT_Utf8_info,針對不同的數據類型,會有不同規則的描述信息。

1)對于基本數據類型來說,使用一個字符來表示,比如說 I 對應的是 int,B 對應的是 byte。

2)對于引用數據類型來說,使用 L***; 的方式來表示,L 開頭,; 結束,比如字符串類型為 Ljava/lang/String;

3)對于數組來說,會用一個前置的 [ 來表示,比如說字符串數組為 [Ljava/lang/String;

對應到 class 文件中的位置如下圖所示。

07、方法表

方法表和字段表類似,區別是用來存儲方法的信息,包括方法名,方法的參數,方法的簽名。

就拿 main 方法來說吧。

public class MethodsTest {
    public static void main(String[] args) {
        
    }
}

先用 jclasslib 看一下大概的信息。

  • 訪問標記是 public static 的。
  • 方法名為 main。
  • 方法的參數為字符串數組;返回類型為 Void。

對應到 class 文件中的位置如下圖所示。

08、屬性表

屬性表是 class 文件中的最后一部分,通常出現在字段和方法中。

來看這樣一段代碼。

public class AttributeTest {
    public static final int DEFAULT_SIZE = 128;
}

只有一個常量 DEFAULT_SIZE,它屬于字段中的一種,就是加了 final 的靜態變量。先通過 jclasslib 看一下它當中一個很重要的屬性——ConstantValue,用來表示靜態變量的初始值。

  • Attribute name index 指向常量池中值為“ConstantValue”的常量。
  • Attribute length 的值為固定的 2,因為索引只占兩個字節的大小。
  • Constant value index 指向常量池中具體的常量,如果常量類型為 int,指向的就是 CONSTANT_Integer_info

我畫了一副圖,可以完整的表示字段的結構,包含屬性表在內。

對應到 class 文件中的位置如下圖所示。

來看下面這段代碼。

public class MethodCode {
    public static void main(String[] args) {
        foo();
    }

    private static void foo() {
    }
}

main 方法中調用了 foo 方法。通過 jclasslib 看一下它當中一個很重要的屬性——Code, 方法的關鍵信息都存儲在里面。

  • Attribute name index 指向常量池中值為“Code”的常量。
  • Attribute length 為屬性值的長度大小。
  • bytecode 存儲真正的字節碼指令。
  • exception table 表示方法內部的異常信息。
  • maximum stack size 表示操作數棧的最大深度,方法執行的任意期間操作數棧深度都不會超過這個值。
  • maximum local variable 表示臨時變量表的大小,注意,并不等于方法中所有臨時變量的數量之和,當一個作用域結束,內部的臨時變量占用的位置就會被替換掉。
  • code length 表示字節碼指令的長度。

對應 class 文件中的位置如下圖所示。

到此為止,class 文件的內部算是剖析得差不多了,希望能對大家有所幫助。第一次拿刀,手有點顫,如果哪里有不足的地方,歡迎大家在評論區毫不留情地指出來!

超級硬核,解剖了超長時間,累壞了,記得幫我點贊鼓勵下吧~

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

推薦閱讀更多精彩內容