博客 / 詳情

返回

Class文件結構

初始class文件

Java類文件是包含可在Java 虛擬機 (JVM)上執行的Java 字節碼的文件(具有.class 文件擴展名)。Java 類文件通常由Java 編譯器根據包含 Java 類的 Java 編程語言源文件(.java文件)生成(或者,其他JVM 語言也可用於創建類文件)。如果一個源文件有多個類,則每個類都被編譯成一個單獨的類文件。
Java虛擬機不包括Java語言在內的任何程序語言綁定,它只與"Class文件" 這種特定的二進制文件格式所關聯,Class文件中包含了Java虛擬機指令集、符號表以及若干其他輔助信息。

class文件出現的背景

Java在剛剛誕生之時曾經提出過一個非常著名的宣傳口號 "一次編寫,到處運行(Write Once,Run Anywhere)",這句話充分表達了當時軟件開發人員對衝破平台界限的渴求。在每時每刻都充滿競爭的IT業界,不可能只有Wintel(Windows與Intel的芯片相結合,曾是業界最強大的聯盟)存在,我們也不希望出現只有Wintel而沒有競爭者的世界,各種不同的硬件體系結構、各種不同的操作系統肯定將會長期並存發展。"與平台無關"的理想最終只有實現在操作系統以上的應用層:Oracle公司以及其他虛擬機發行商發佈過許多可以運行在各種不同硬件平台和操作系統上的Java虛擬機,這些虛擬機都可以載入和執行同一種平台無關的字節碼,從而實現了層序的 "一次編寫,到處運行"

Java虛擬機提供的語言無關性

image.png

Class類文件的結構

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

根據《Java 虛擬機規範》的規定,Class文件格式採用一種類似於C語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型:"無符號數""表"。後面的解析都要以這兩種數據類型為基礎,所以這裏需要先明白這兩個概念。

  • 無符號數屬於基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節和8個字節的無符號數,無符號數可以用來描述數字、索引飲用、數值量或者按照UTF-8編碼構成字符串值。
  • 表是由多個無符號數或者其他表作為數據項構成的複合(引用)數據類型,為了便於區分,所有表的命名都習慣性地以 "_info" 結尾。表用於描述有層次關係的複合結構的數據,整個Class文件本質上也可以視作是一張表,這張表有以下所示的數據項按照嚴格順序排列構成
類 型 名稱 數量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u4 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
interfaces_info interfaces interfaces_count
u2 fields_count 1
fields_info fields fields_count
u2 methods_count 1
methods_info methods methods_count
u2 attributes_count 1
attributes_info attributes attributes_count
u2 fields fields_count
u2 fields fields_count

魔數與Class文件的版本

每個Class文件的頭4個字節被稱為魔數(Magic Number),它的唯一作用是確定這個文件是否為一個能被Java虛擬機接受的Class文件。不僅是Class文件,很多文件格式標準中都有使用魔數來進行身份識別的習慣。

0xCAFEBABE(咖啡寶貝?)背景故事?

這個標識的由來是James Gosing在談到帕洛阿爾託的一家餐廳時解釋了這個神奇數字的歷史:"我們過去常去一個叫聖米迦勒巷的地方吃午飯。根據當地傳説,在黑暗的過去,Grateful Dead(1964年組建的一隻美國樂隊)在他們成名之前曾經在那裏演出。這是一個非常時髦的地方,絕對是一個 Grateful Dead Kinda Place,Jerry(樂隊的主唱)死後,他們甚至建了一個佛教風格的小神社,我們以前去那裏的時候,我們稱這個地方為 Cafe Dead。沿線的某個地方注意到這是一個 HEX 數字。我正在重新修改一些文件格式代碼,需要一些神奇的數字:一個用於持久對象文件,一個用於類。我使用 CAFEDEAD 作為對象文件格式,並在grepping對於適合“CAFE”(這似乎是一個很好的主題)之後的 4 個字符的十六進制單詞,我偶然發現了 BABE 並決定使用它。在那個時候,除了歷史的垃圾桶之外,它似乎並沒有什麼特別的重要或註定要去任何地方。所以 CAFEBABE 成為類文件格式,而 CAFEDEAD 成為持久對象格式。但是持久對象工具消失了,隨之而來的是 CAFEDEAD 的使用——它最終被RMI取代。"

緊接着魔數的4個字節存儲的是Class文件的版本號:第5和第6個字節都是次版本號(Minor Version),第7和第8個字節是主版本號(Major Version)。Java的版本號是從45開始的,JDK1.1之後的每個JDK大版本發佈主版本號向上加1(JDK 1.0 ~ 1.1使用了45.0 ~ 45.3的版本號),高版本的JDK能向下兼容以前版本的Class文件,但不能運行以後版本的Class文件,因為《Java虛擬機規範》在Class文件校驗部分明確要求了即使文件格式並未發生任何變化,虛擬機也必須拒絕執行超過其版本號的Class文件。

為了説明以上魔數和版本,本人做了一段最簡單的Java代碼(如代碼4.1),後面的內容都將以這段程序使用JDK9編譯輸出的Class文件為基礎來進行演示。


package com.yzr.jvm.classf;

public class TestClass {

    private int a;

    public int increment() {

        return a + 1;
    }

}

圖4.2顯示的是使用十六進制編輯器 Sublime Text 打開這個類編譯後的結果,可以清楚的看見開頭四個字節的十六進制表示是0xCAFEBABE(兩位十六進制代表8bit),代表次版本號的第5個和第6個字節值為0x0000,而主版本號的值為0x0034,也即是十進制的52(可通過在線轉換工具進行進制轉換:在線進制轉換),該版本號説明這個是可以被JDK6或以上版本虛擬機執行的Class文件。

image.png
圖4.2

以下表格列出了從JDK1.1到13之間,主流JDK版本編譯器輸出的默認的和可支持的Class文件版本號。

JDK 版本 -target參數 -source參數 版本號 |
JDK 1.1.8 不支持target參數 不支持resource參數 45.3
JDK 1.2.2 不帶(默認為-target 1.1) 1.1~1.2 45.3
JDK 1.2.2 -target 1.2 1.1~1.2 46.0
JDK 1.3.1_19 不帶(默認為-target 1.1) 1.1 ~ 1.3 45.3
JDK 1.3.1_19 -target 1.3 1.1 ~ 1.3 47.3
JDK 1.4.2_10 不帶(默認為-target 1.2) 1.1 ~ 1.4 46.0
JDK 1.4.2_10 -target 1.4 1.1 ~ 1.4 48.0
JDK 5.0_11 不帶(默認為-target 1.1),後續版本不帶target參數默認編譯的Class文件均與其JDK版本相同 1.1 ~ 1.5 49.0
JDK 5.0_11 -target 1.4 -source 1.4 1.1 ~ 1.5 48.0
JDK 6 不帶(默認為-target 6) 1.1 ~ 6 50.0
JDK 7 不帶(默認為-target 7) 1.1 ~ 7 51.0
JDK 8 不帶(默認為-target 8) 1.1 ~ 8 42.0
JDK 9 不帶(默認為-target 9) 6 ~ 9 53.0
JDK 10 不帶(默認為-target 10) 6 ~ 10 54.0
JDK 11 不帶(默認為-target 11) 6 ~ 11 55.0
JDK 12 不帶(默認為-target 12) 6 ~ 12 56.0
JDK 13 不帶(默認為-target 13) 6 ~ 13 57.0

常量池

緊接着主、次版本之後的是常量池入口,常量池可以比喻為Class文件裏的資源倉庫,它是Class文件結構中與其他項目關聯最多的數據,通常也是佔用Class文件空間最大的數據項之一,另外,它還是Class文件中第一個出現的表類型數據項目。

由於常量池中的常量數量是不固定的,所以在常量池的入口需要放置一項u2(2字節)類型的數據,代表常量池容量計數值(constat_pool_count)。與Java中語言習慣不同,這個容量計數是從1而不是0開始的,如下圖4-3所示,常量池容量(便宜地址:0x000000008)為十六進制數0x0016,即十進制的22,這就代表常量池中有21項常量,索引值範圍為1~21.在Class文件格式規範制定之時,設計者將第0項常量空出來是有特殊考慮的,這樣做的目的在於,如果後面某些指向常量池的索引值的數據在特定情況下需要表達 "不引用任何一個常量池項目" 的含義,可以把索引值設置為0來表示。Class文件結構中只有常量池的容量計數是從1開始,對於其他集合類型,包括接口索引集合、字段表集合、方法表集合等的容量計數都與一般相同,是從0開始。

image.png

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

  • 被模塊導出或者開放的包(Package)
  • 類和接口的全限定名(Fully Qualified Name)
  • 字段的名稱和描述符(Descriptor)
  • 方法的名稱和描述符
  • 方法句柄和方法類型(Method Handle、MethodType、Invoke Dynamic)
  • 動態調用點和動態常量(Dynamically-Computed Call Site、Dynamically-Computed-Constant)

Java代碼在進行Javac編譯的時候,在虛擬機加載Class文件的時候進行動態鏈接。也就是説,在Class文件中不保存各個方法、字段最終在內存中的佈局信息,這些字段、方法的符號引用不僅過虛擬機在運行期轉換的話是無法得到真正的內存入口地址,也就無法直接被虛擬機使用的。當虛擬機做類加載時,將會從常量池中獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址之中。

常量池中每一項常量都是一個表,截止JDK13,常量表中分別有17種不同類型的常量。這17類表都有一個共同特點,表結構其實的第一位是一個u1(1個字節)類型的標誌位(tag,取值表見下表格中標誌列),代表着當前常量屬於哪種常量類型。17中常量類型具體含義如下:

類 型 標 志 描述
CONSTANT_Utf8_info 1 UTF-8 編碼的字符串
CONSTANT_Integer 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_Module_info 19 表示一個模塊
CONSTANT_Package_info 20 表示一個模塊中開發或者導出的包

請查看下圖中的常量池的第一項常量,它的標誌位是0x0a,通過查表的標誌列可知這個常量是CONSTANT_Methodref_info類型,此類型的常量代表類方法的符號引用。CONSTANT_Methodref_info結構如下:

類型 名稱 數量
u1 tag 1
u2 class_index 1
u2 name_index 1

tag是標誌位,它用於區分常量類型;class_index是常量池的索引值,它指向常量池中一個CONSTANT_Utf8_info類型常量,此常量指向了聲明方法的類描述符CONSTANT_Class_info的索引項本例中的class_index值為0x0004,也就是指向了常量池中的第二項常量。繼續從上圖中查找第二項常量,它的標誌位是

訪問標誌

類索引、父類索引與接口索引集合

字段表集合

方法表集合

屬性表集合

Code屬性

待更新...

擴展知識

Grateful Dead

user avatar tracy_5cb7dfc1f3f67 頭像 gozhuyinglong 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.