前言
為了研究Class檔案,先編寫一個最簡單的程式碼:
package com.courage;
public class T0100_ByteCode01 {
}
之所以說最簡單,是因為這個類裡面任何方法,變數都沒有,看看編譯之後Class檔案的16進位制程式碼:
在解讀上面的Class檔案(後面沒有特殊生命的話都是指16進位制)之前,需要先學習幾個前置知識,Java 虛擬機器規範規定 Class 檔案格式採用一種類似與 C 語言結構體的微結構體來儲存資料,這種偽結構體中只有兩種資料型別:無符號數和表。
- 無符號數屬於基本的資料型別,以 u1、u2、u4、u8來分別代表 1 個位元組、2 個位元組、4 個位元組和 8 個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照 UTF-8 編碼結構構成的字串值。
- 表是由多個無符號數或者其他表作為資料項構成的複合資料型別,所有表都習慣性地以「_info」結尾。表用於描述有層次關係的複合結構的資料,整個 Class 檔案就是一張表,它由下表中所示的資料項構成。
有了無符號數這個概念,就可以根據虛擬機器規範來解讀上面的檔案了:
型別 | 名稱 | 含義 | 數量 |
---|---|---|---|
u4 | magic | 魔數,不變 | 1 |
u2 | minor_version | 小版本號:JDK 8_255u中的255u | 1 |
u2 | major_version | 大版本號,JDK 8_255u中的8 | 1 |
u2 | constant_pool_count | 常量池數量 | 1 |
cp_info | constant_pool | 常量池 | constant_pool_count-1 |
u2 | access_flags | 訪問修飾符 public static 等 | 1 |
u2 | this_class | 當前類 | 1 |
u2 | super_class | 父類 | 1 |
u2 | interfaces_count | 介面數量 | 1 |
u2 | interfaces | 介面 | interfaces_count |
u2 | fields_count | 變數數量 | 1 |
field_info | fields | 變數 | fields_count |
u2 | methods_count | 方法數量 | 1 |
method_info | methods | 方法 | methods_count |
u2 | attributes_count | 屬性數量 | 1 |
attribute_info | attributes | 屬性 | attributes_count |
所有的Class檔案裡面的屬性都按照上表的規則排序,中間沒有空行或其他轉義字元。哪個位元組代表什麼含義,長度是多少,先後順序如何都是被嚴格限制的,不允許有任何改變。
魔數與 Class 檔案版本
每個 Class 檔案的頭 4 個位元組稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接收的 Calss 檔案。之所以使用魔數而不是檔案字尾名來進行識別主要是基於安全性的考慮,因為檔案字尾名是可以隨意更改的。Class 檔案的魔數值為「0xCAFEBABE」。
緊接著魔數的 4 個位元組儲存的是 Class 檔案的版本號:第 5 和第 6 兩個位元組是次版本號(Minor Version),第 7 和第 8 個位元組是主版本號(Major Version)。高版本的 JDK 能夠向下相容低版本的 Class 檔案,虛擬機器會拒絕執行超過其版本號的 Class 檔案。
常量池
主版本號之後是常量池入口,常量池可以理解為 Class 檔案之中的資源倉庫,它是 Class 檔案結構中與其他專案關聯最多的資料型別,也是佔用 Class 檔案空間最大的資料專案之一,同是它還是 Class 檔案中第一個出現的表型別資料專案。
因為常量池中常量的數量是不固定的,所以在常量池入口需要放置一個 u2 型別的資料來表示常量池的容量「constant_pool_count」,和電腦科學中計數的方法不一樣,這個容量是從 1 開始而不是從 0 開始計數。之所以將第 0 項常量空出來是為了滿足後面某些指向常量池的索引值的資料在特定情況下需要表達「不引用任何一個常量池專案」的含義,這種情況可以把索引值置為 0 來表示。
Class 檔案結構中只有常量池的容量計數是從 1 開始的,其它集合型別,包括介面索引集合、欄位表集合、方法表集合等容量計數都是從 0 開始。
常量池中主要存放兩大類常量:字面量和符號引用。
-
字面量比較接近 Java 語言層面的常量概念,如字串、宣告為 final 的常量值等。
-
符號引用屬於編譯原理方面的概念,包括了以下三類常量:
- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
常量池17種資料型別的結構表
訪問標誌
緊接著常量池之後的兩個位元組代表訪問標誌(access_flag),這個標誌用於識別一些類或者介面層次的訪問資訊,包括這個 Class 是類還是介面;是否定義為 public 型別;是否定義為 abstract 型別;如果是類的話,是否被申明為 final 等。具體的標誌位以及標誌的含義見下表:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為 public 型別 |
ACC_FINAL | 0x0010 | 是否被宣告為 final,只有類可設定 |
ACC_SUPER | 0x0020 | 是否允許使用 invokespecial 位元組碼指令的新語意,invokespecial 指令的語意在 JKD 1.0.2 中發生過改變,微聊區別這條指令使用哪種語意,JDK 1.0.2 編譯出來的類的這個標誌都必須為真 |
ACC_INTERFACE | 0x0200 | 標識這是一個介面 |
ACC_ABSTRACT | 0x0400 | 是否為 abstract 型別,對於介面或者抽象類來說,此標誌值為真,其它類值為假 |
ACC_SYNTHETIC | 0x1000 | 標識這個類並非由使用者程式碼產生 |
ACC_ANNOTATION | 0x2000 | 標識這是一個註解 |
ACC_ENUM | 0x4000 | 標識這是一個列舉 |
access_flags 中一共有 16 個標誌位可以使用,當前只定義了其中的 8 個,沒有使用到的標誌位要求一律為 0。
類索引、父類索引與介面索引集合
類索引(this_class)和父類索引(super_class)都是一個 u2 型別的資料,而介面索引集合(interfaces)是一組 u2 型別的資料集合,Class 檔案中由這三項資料來確定這個類的繼承關係。
- 類索引用於確定這個類的全限定名
- 父類索引用於確定這個類的父類的全限定名
- 介面索引集合用於描述這個類實現了哪些介面
欄位表集合
欄位表集合(field_info)用於描述介面或者類中宣告的變數。欄位(field)包括類變數和例項變數,但不包括方法內部宣告的區域性變數。下面我們看看欄位表的結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
欄位修飾符放在 access_flags 中,它與類中的 access_flag 非常相似,都是一個 u2 的資料型別。
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 欄位是否為 public |
ACC_PRIVATE | 0x0002 | 欄位是否為 private |
ACC_PROTECTED | 0x0004 | 欄位是否為 protected |
ACC_STATIC | 0x0008 | 欄位是否為 static |
ACC_FINAL | 0x0010 | 欄位是否為 final |
ACC_VOLATILE | 0x0040 | 欄位是否為 volatile |
ACC_TRANSIENT | 0x0080 | 欄位是否為 transient |
ACC_SYNTHETIC | 0x1000 | 欄位是否由編譯器自動生成 |
ACC_ENUM | 0x4000 | 欄位是否為 enum |
方法表集合
Class 檔案中對方法的描述和對欄位的描述是完全一致的,方法表中的結構和欄位表的結構一樣。
因為 volatile 關鍵字和 transient 關鍵字不能修飾方法,所以方法表的訪問標誌中沒有 ACC_VOLATILE 和 ACC_TRANSIENT。與之相對的,synchronizes、native、strictfp 和 abstract 關鍵字可以修飾方法,所以方法表的訪問標誌中增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 標誌。
對於方法裡的程式碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表中一個名為「Code」的屬性裡面。
屬性表集合
在 Class 檔案、欄位表、方法表中都可以攜帶自己的屬性表(attribute_info)集合,用於描述某些場景專有的資訊。
屬性表集合不像 Class 檔案中的其它資料項要求這麼嚴格,不強制要求各屬性表的順序,並且只要不與已有屬性名重複,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java 虛擬機器在執行時會略掉它不認識的屬性。
下面就可以對類檔案逐行分析了: