圖解 | 原來這就是 class

閃客sun發表於2021-03-25

我是一個 .java 檔案,名叫 FlashObject.java,叫我小渣就行。

public class FlashObject {

    private String name;
    private int age;
    
    public String getName() {
        return name;
    }

    public int add(int a, int b) {
        return a + b;
    }

}

我馬上就要被 JVM 虛擬機器老大載入並執行了,此時 老虛 走了過來。

老虛:小渣呀,我馬上就要把你載了,你先瘦身一下,別佔太大地方。

小渣:好的,沒問題,等我十秒鐘。

public class FlashObject{private String name;private int age;public int add(int a,int b){return a+b;}

小渣:老虛,我瘦身好了,你看看。

老虛:...,你是不是有病。

小渣:怎麼了,我把沒用的空格和回車啥的都去掉了,瘦身了好多呢!

老虛:行吧,看你這智商,我就給你解釋解釋。你現在仍然是個文字檔案,讓你瘦身是讓你定一個緊湊的資料結構來表示你這個 Java 檔案裡的資訊,然後告訴我這個資料結構中每個位元組都代表什麼。

小渣:哦哦,這樣啊。

老虛:對啊,這樣一是方便我去載入,二是我這個虛擬機器可不只是為你 Java 語言服務的,還有很多語言最終都可以轉換為我虛擬機器識別的,你得設計一個通用的格式。

小渣:嗯嗯,這回我明白啦!  

1 類資訊

  我的類名叫 FlashObject。 先找個地方把它存起來,放開頭吧。

圖解 | 原來這就是 class

這裡的一個小方格是 1 個位元組,也就是 8 位。一個英文字母用 ASCII 碼錶示為 1 個位元組,所以佔一個方格,之後不再解釋。

嚴謹的我又想到,這個類應該還有其父類 雖然這個 .java 檔案中沒寫,但也有其預設父類,Object。 當然,我們得記錄下全類名 java/lang/Object 記在哪裡呢?就緊跟在類名後面吧。

圖解 | 原來這就是 class

誒不對,我這個類名呀,父類名呀,都是變長的,這樣緊挨著放,誰知道分界點在哪。 不行不行,得分別在前面加個長度,就用兩位元組表示吧。 圖解 | 原來這就是 class

除了父類之外,還有介面名呢!雖然我們這個類沒寫,但也得定義出來。 這個介面,和類名以及父類名稍有不同,因為可能有多個。 但這不是事兒,先佔用兩個位元組,表示介面的數量即可,之後一個一個的介面名仍然像上面那樣緊挨著排布。

圖解 | 原來這就是 class

嗯,完美。

2 常量池


慢慢地,我發現需要字串名字的地方越來越多。 除了剛剛的類名、父類名、介面名,還有屬性名、方法名、屬性的類名、方法的入參型別名、返回值型別名,等等等等。 一方面,要是每個都這麼展開寫下去,那檔案格式會很亂,很多結構都是變長的。 另一方面,很多字串都是重複的,比如屬性 name 的類名 String,與方法 getName 的返回值類名 String,重複寫兩遍,就浪費了空間。 因此,我決定,之前的方案作廢,設計一個新的結構來統一儲存這些字串,我給他起名為常量池

圖解 | 原來這就是 class

每個字串都有一個索引與之對應,這個是可以計算出來的,不需要額外的欄位。

圖解 | 原來這就是 class

這樣,剛剛的類、父類、介面,就都可以指向這個索引了,也因此可以將長度固定下來。 圖解 | 原來這就是 class

當然,現在這個常量池,僅僅存放了字串。 不難想到,還可能有整型、浮點型的值作為常量,甚至還有可能是個引用型別,然後這個引用型別再次指向常量池中的一個索引,有點像指標的指標。 那這麼多型別,必然就還需要一個記錄型別資訊的地方,看來我們得將之前的設計改改。

圖解 | 原來這就是 class

這樣,我們的常量池,就不單單可以儲存簡單的字串常量了,而是可以根據不同型別,儲存與其相對應的資料結構的值。 當然,我們常量池的整體結構還是不變的,只不過裡面是型別豐富的結構。

圖解 | 原來這就是 class

同樣,我們的整個設計,也沒有因為常量池的小改動,受到影響。

圖解 | 原來這就是 class

OK,總結一下我們目前的整體方案。 開頭存常量池,之後需要的常量就全往這裡放,用一個索引指向它即可。 緊接著存放類本身的相關資訊,我們存放了當前類、父類以及介面的資訊。 看來老虛要求的瘦身工作,已經初具規模啦。  

3 變數

  現在類本身的資訊,已經找到合適的位置存放起來了,接下來我們存變數。 變數也可能有多個,所以結構依然仿照我們之前的思路,開頭存數量,後面緊跟著各個存放變數的資料結構。

圖解 | 原來這就是 class

至於變數用什麼資料結構來存,是不是定長的,那就是我們接下來要設計的了。 我們把其中一個變數拿出來,看看它有什麼? private String name; 非常清晰,private 這部分是變數的標記,String 是變數型別,name 是變數名字。

先看標記部分

除了 private,還有 public、protected、static、final、volatile、transient 等,有的可以放在一起,比如 public static final String name; 有的不能放在一起,比如 public private String name; //錯誤 我們用點陣圖的方式,每一個標記用一個位來表示 (比如 public 在第一個位,private 在第二個位,static 在第四個位,final 在第五個位...) ,這樣不論如何排列組合,最終的值都是不一樣的。

圖解 | 原來這就是 class

我們把這些標記所對應的值,都設計並記錄下來。

標記

public

0x0001

private

0x0002

protected

0x0004

static

0x0008

final

0x0010

volatile

0x0040

transient

0x0080

複合型的標記,就可以表現為將其相加,比如 public static,就是 0x0001 + 0x0008 = 0x0009。 而這樣的賦值方式,不同排列組合後的和沒有重複的,且也能根據值很方便地反推出標記。 不錯不錯,就這樣了。 哦對了,類資訊本身也有 public 呀 private 這些標記屬性,剛剛記錄類資訊的時候忘了,先加上它,免得一會忘了! 圖解 | 原來這就是 class

再看型別部分

當前型別為 String,屬於一個引用資料型別中的類型別 private String name; 除此之外,還有八個基本資料型別,和引用型別中的陣列型別 為了佔用更少的空間,我們將其用最少的符號來表示。

符號表示

型別

B

byte

C

char

D

double

F

float

I

int

J

long

S

short

Z

boolean

LClassName ;

[

陣列

裡的基本資料型別,和陣列型別 ,都只佔用一個 char 來表示,就只佔了 1 個位元組。 如果是類,則佔用了 L 和 ; 兩個位元組,再加上全類名所佔的位元組數。 比如這裡的 String 型別,用符號表示,就是 Ljava/lang/String; 但注意,這裡的符號,也都可以存放在常量池中,而我們的變數結構中的型別描述符部分,只需要一個常量池索引即可。

圖解 | 原來這就是 class

ok,第二部分也搞定了。

再看名字部分

名字部分沒什麼好說的,相信你直接能猜到了,直接上圖。

圖解 | 原來這就是 class

OK,兩位元組的標記、兩位元組的型別描述符、兩位元組的變數名稱,這個就是我們一個變數的資料結構。

圖解 | 原來這就是 class

把它放到我們最終的總檢視裡。

圖解 | 原來這就是 class 搞定!

4 方法


方法也可能會有很多,我目前只有兩個方法,我們拿 add 方法來分析。

public int add(int a, int b) {
    return a + b;
}

當然更準確地說,我還有個沒寫出來的構造方法。 總之,可能會有很多。 不過有了設計變數的經驗,方法的資料結構很快就有了雛形。

圖解 | 原來這就是 class

標記部分 ,和變數標記部分的思路一樣,值也差不多,我們也給他們賦上值就好了。

標記

public

0x0001

private

0x0002

protected

0x0004

static

0x0008

final

0x0010

volatile

0x0040

transient

0x0080

synchronized

0x0020

native

0x0100

abstract

0x0400

方法描述符 ,說的是方法的入參與返回值,比如我們的: int add(int a, int b); 入參與返回值的型別符號表示,與上面變數型別的符號表示完全一樣,只不過多了一個 void 型別。

符號表示

型別

B

byte

C

char

D

double

F

float

I

int

J

long

S

short

Z

boolean

LClassName ;

[

陣列

V

void

由於有多個引數型別,所以要定一個整體的格式,而整個描述符的格式為: ( 引數1型別 引數2型別 ... ) 返回值型別 比如我們的 int add(int a, int b); 就表示為 (II)I 是不是非常精簡了?同樣,這也是個字串,也可以儲存在常量池裡,就不再贅述。 (至於引數 a 和 b 這個名字,不需要儲存起來,實際上在轉換的位元組碼以及實際虛擬機器中執行時,只需要知道區域性變數表中的位置即可,叫什麼名字都無所謂)

方法名稱 ,我們再熟悉不過了,放常量池! ok,前三個說完了。最後一個,就有意思了。

圖解 | 原來這就是 class

程式碼、異常、註解等。 以看到,有相當多的資訊需要記錄。 比如我寫這樣的方法。

@RequestMapping()
public String function(String a) throws Exception {
    return a;
}

那就會有程式碼部分、異常、註解等需要錄入的資訊。 但似乎除了程式碼部分之外,其他部分都不是每個方法都有的,如果都定義出來,豈不是浪費空間,那怎麼辦呢? 我們效仿常量池的做法,把這些部分都叫“方法的屬性”,一個方法可能有多個屬性,設計結構如下。

圖解 | 原來這就是 class

這樣,方法具有哪些屬性,按需新增進來就好,如果不需要這個屬性,也不用浪費空間,完美! 回過頭看我們的這個方法。

public int add(int a, int b) {
    return a + b;
}

剛剛方法簽名部分已經都解決了,只剩下程式碼 return a + b; 這個要怎樣存放呢? 之前聽老虛說過,JVM 識別的是一種叫位元組碼的東西,所以我要把 Java 語言寫出的程式碼,轉換為位元組碼。 這部分很複雜,就不展開說我的過程了,經過一番努力後,我把這一行簡簡單單的程式碼轉換為了位元組碼。 1B 1C 60 AC 一共佔四個位元組。 我把這四個位元組,就放在剛剛程式碼型別的屬性中。

圖解 | 原來這就是 class

ok,大功告成。 回過頭,我們將之前的方法部分補充完整。

圖解 | 原來這就是 class

再將這個結構,新增到我們全域性結構中。

圖解 | 原來這就是 class

完美!

5 class


我把我轉換為了這樣的結構,並帶著這個最終的設計稿,去找了老虛。

老虛:嗯!還真不賴!

小渣:那當然,我可是研究了好久呢。

老虛:不過,我再給你改改,在開頭加些東西把。

圖解 | 原來這就是 class

小渣:老虛,你這加的是啥呀?
老虛:一看你就沒經驗。 魔數 一般用來識別這個檔案的格式,通過檔名字尾的方式不靠譜,一般有格式的檔案都會有個魔數的。 後面兩個用來標識一下版本號,不同版本可能資料結構和支援的功能不一樣,這個今後會有用的!

小渣:原來如此,還是你老虛見多識廣。可是你說用來識別這個檔案的格式,我這個檔案是啥呀?

老虛:你這個破玩意,就叫它 class 檔案吧!

FlashObject.class

後記

 

根據 Java 虛擬機器規範,Java Virtual Machine Specification Java SE 8 Edition,一個 class 檔案的標準結構,是這樣的。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

我們的設計與它幾乎相同。

只有後兩項,我們沒有涉及到,本身也不是重點。

常量池中的型別,有以下幾種。

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

如果想了解 class 檔案的全部細節,最好的辦法就是閱讀官方文件,也就是 Java 虛擬機器規範的第四部分。

Chapter 4. The class File Format

這裡的連結可以直接定位:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

不要覺得官方文件晦澀難懂,這個部分還是非常清晰明瞭的,大多數部落格基本上對格式的講解都缺斤少兩,而且說得也不形象,還不如直接閱讀官方文件呢。

還有一個好的方式,就是直接觀察 class 檔案的二進位制結構解析,這裡推薦一個工具

classpy

用這個工具開啟一個 class 檔案,是這個樣子。

圖解 | 原來這就是 class

左邊解析好的樹型結構,可以直接和右邊的 class 檔案的二進位制內容相對應,非常好用。

最後,希望大家找時間用這個工具分析一個複雜的 class 檔案,會很有幫助的。祝大家學會 class 檔案。

相關文章