Class 類文件結構

本文部分摘自《深入理解 Java 虛擬機第三版》

概述

我們知道,Java 具有跨平台性,其實現基礎就是虛擬機和位元組碼存儲格式。Java 虛擬機不與 Java 語言綁定,只與 Class 文件所關聯。Java 虛擬機作為一個通用的、與機器無關的執行平台,任何語言都可以將 Java 虛擬機作為它們的運行基礎,以 Class 文件作為它們產品的交付媒介。

Class 文件是一組以 8 個位元組為基礎單位的二進位流,各個數據項目嚴格按照順序緊湊地排列在文件之中,中間沒有添加任何分隔符,這使得整個 Class 文件中存儲的內容幾乎全部是程式運行的必要數據。當遇到需佔用 8 個位元組以上空間的數據項時,則會按照高位在前的方式分割成若干個 8 個位元組進行存儲。

Class 文件中有兩種數據類型,分別是無符合數和表:

  • 無符號數屬於基本數據類型,以 u1、u2、u4、u8 來分別代表 1 個位元組、2 個位元組、4 個位元組和 8 個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或 UTF-8 編碼構成字元串值
  • 表是由多個無符號數或其他表作為數據項構成的複合數據類型,一般以 _info 結尾。表用於描述有層次關係的複合結構的數據,整個 Class 文件本質上也可以視作是一張表

無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這時候稱這一系列連續的某一類型的數據為某一類型的集合。

下面是 Class 文件格式:

類型 名稱 數量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count – 1
u2 access_flags 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 attribute_count 1
attribute_info attributes attributes_count

魔數和 Class 文件版本

Class 文件的頭 4 個位元組被稱為魔數(Magic Number),它的唯一作用是確定該 Class 文件是否能被虛擬機接受,其值為 0xCAFEBABE(咖啡寶貝)。

緊接著魔數的 4 個位元組存儲的是 Class 文件的版本號:第 5 和第 6 個位元組是次版本號(Minor Version),第 7 和第 8 個位元組是主版本號(Major Version)。Java 版本號從 45 開始,以後每個 JDK 大版本發布則主版本號加 1。高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能運行以後版本的 Class 文件。

常量池

緊接著主、次版本號的是常量池入口,常量池入口需要放置一項 u2 類型的數據,代表常量池容量計數值(constant_pool_count),這個容量計數是從 1 而不是從 0 開始,第 0 項用於表達「不引用任何一個常量池項目」的含義。Class 文件結構中只有常量池的容量計數是從 1 開始,其他都是從 0 開始。

常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近 Java 語言層面的常量概念,如文本字元串、被聲明為 final 的常量值等。而符號引用則屬於編譯原理方面的概念,主要包括下面幾類常量:

  • 被模組導出或開放的包(Package)
  • 類和介面的全限定名(Fully Qualified Name)
  • 欄位的名稱和描述符(Descriptor)
  • 方法的名稱和描述符
  • 方法句柄和方法類型(Method Handle、Method Type、Invoke Dynamic)
  • 動態調用點和動態常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

Java 會在虛擬機載入 Class 文件的時候進行動態連接,將符號引用轉換為真正的記憶體入口。常量池中每一項常量都是一個表,最初有 11 種結構不同的表結構數據,後來為了更好地支援動態語言調用,額外增加了 4 種動態語言相關的常量,後來為了支援 Java 模組化,又加入了 2 個常量,所以截止 JDK13,常量表中有 17 種不同類型的常量。這 17 類表都有一個共同的特點,表結構起始的第一位是個 u1 類型的標誌位(tag)

17 種常量類型所代表的具體含義如表:

類型 標誌 描述
CONSTANT_Utf8_info 1 UTF-8 編碼的字元串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮點型字面量
CONSTANT_Long_info 5 長整型型字面量
CONSTANT_Double_info 6 雙精度浮點型字面量
CONSTANT_Class_info 7 類或介面的符號引用
CONSTANT_String_info 8 字元串類型字面量
CONSTANT_Fieldref_info 9 欄位的符號引用
CONSTANT_Methodref_info 10 類中方法的符號引用
CONSTANT_InterfaceMethodref_info 11 介面中方法的符號引用
CONSTANT_NameAndType_info 12 欄位或方法的部分符號引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法類型
CONSTANT_Dynamic_info 17 表示一個動態計算常量
CONSTANT_InvokeDynamic_info 18 表示一個動態方法調用點
CONSTANT_Moudle_info 19 表示一個模組
CONSTANT_Package_info 20 表示一個模組中開放或者導出的包

訪問標誌

常量池結束之後,緊接著的 2 個位元組代表訪問標誌(access_flags),這個標誌用於標識一些類或者介面層次的訪問資訊,包括這個 Class 是類還是介面;是否定義為 public 類型;是否定義為 abstract 類型;如果是類的話,是否被聲明為 final 等等。具體的標誌位以及標誌的含義如表:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 是否為 Public 類型
ACC_FINAL 0x0010 是否被聲明為 final,只有類可以設置
ACC_SUPER 0x0020 是否允許使用 invokespecial 位元組碼指令的新語義
ACC_INTERFACE 0x0200 標誌這是一個介面
ACC_ABSTRACT 0x0400 是否為 abstract 類型,對於介面或者抽象類來說,次標誌值為真,其他類型為假
ACC_SYNTHETIC 0x1000 標誌這個類並非由用戶程式碼產生
ACC_ANNOTATION 0x2000 標誌這是一個註解
ACC_ENUM 0x4000 標誌這是一個枚舉

access_flags 中一共有 16 個標誌位可以使用,當前只定義了其中 9 個,沒有使用到的標誌位要求一律為零。

類索引、父類索引與介面索引集合

類索引(this_class)、父類索引(super_class)和介面索引(interfaces)都按順序排列在訪問標誌之後,類索引和父類索引用兩個 u2 類型的索引值表示,而介面索引是一組 u2 類型的數據的集合。

類索引用於確定該類的全限定名,父類索引確定該類的父類的全限定名,由於 Java 不允許多繼承,因此父類索引只有一個,Object 類的父類索引為 0。類索引和父類索引各自指向一個 CONSTANT_Class_info 的類描述符常量,通過這個索引值可以找到定義在 CONSTANT_Utf8_info 類型的常量中的全限定名字元串。

對於介面索引集合,入口的第一項 u2 類型的數據為介面計數器(interfaces_count),表示索引表的容量,如果該類沒有實現任何介面,則該計數器的值為 0,後面介面的索引表不再佔用任何位元組。

欄位表集合方法表集合

欄位表集合(field_info)用於描述介面或類中聲明的變數,包括類級變數以及實例級變數,但不包括在方法內部聲明的局部變數。欄位包含待資訊有欄位的作用域(public、private、protected)、是實例變數還是類變數(static)、可變性(final)等等。這些資訊要麼有,要麼沒有,很適合用標誌位來表示,而欄位叫什麼,被定義為什麼數據類型,這些都無法固定,只能用常量池中的常量來描述。

欄位表結構如下:

類型 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

access_flags 用來標識欄位修飾符(public、static、final、volatile …),name_index 和 descriptor_index 都是對常量池的引用,分別代表欄位的簡單名稱以及欄位和方法的描述符。之後的是屬性表集合,用於存儲一些額外資訊。

欄位表集合不會列出從父類或父介面繼承而來的欄位,但有可能出現原本 Java 程式碼中沒有的欄位,例如內部類為了保持對外部類的訪問性,編譯器會自動添加指向外部類實例的欄位。

方法表集合與欄位表集合幾乎完全一致,僅在標誌和屬性表集合的可選項中有所區別。至於方法裡面的程式碼,則經編譯後存放在方法屬性表集合中一個名為 Code 的屬性裡面。

Tags: