JAVA: 一把小刀,直插 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 這個命令的神奇之處就是它能將一個給定文件轉換成十六進制形式。

魔數

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

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

版本號

緊跟着魔數後面的四個字節 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 月份推出。

常量池

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

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_info、CONSTANT_Long_info、CONSTANT_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_info 、CONSTANT_Methodref_info 和 CONSTANT_InterfaceMethodref_info,它們三個的結構比較類似,可以通過下面的僞代碼來表示。

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

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

還有 CONSTANT_MethodHandle_info 、CONSTANT_MethodType_info 和 CONSTANT_InvokeDynamic_info,我就不再一一說明了,大家也可以拿把小刀去試一試。

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

訪問標記

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

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

public enum Color {
    RED,GREEN,BLUE;
}

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

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

this_class、super_class、interfaces

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

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

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

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

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

字段表

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

來看這樣一段代碼。

public class FieldsTest {
    private String name;
}

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

field_info {
  u2 access_flag;
  u2 name_index;
  u2 description_index;
}

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

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

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

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

方法表

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

就拿 main 方法來說吧。

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

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

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

屬性表

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

來看這樣一段代碼。

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

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

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

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

來看下面這段代碼。

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

    private static void foo() {
    }
}

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

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

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/yDf3UQMiQzwCUV5urtr4sA