Class檔案結構全面解析(上)
什麼是Class檔案?
在Java剛剛誕生的時候就提出了一個非常著名的口號:“一次編寫,到處執行。(Write Once,Run Anywhere)”。為了實現平臺無關性,各種不同平臺的虛擬機器都統一使用一種程式儲存格式,就是位元組碼(ByteCode)。它就以二進位制位元組流的方式被存放在Class檔案中,其中包含了Java虛擬機器指令集和符號表以及其他輔助資訊。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
為什麼需要了解Class檔案結構?
一般對於資料結構的分享難免比較枯燥,但是瞭解Class檔案結構是瞭解Java虛擬機器的重要基礎之一。如果想比較深入地瞭解Java虛擬機器,那麼Class檔案結構是不能不接觸的。我會力求在保證邏輯準確的基礎上,儘量通俗易懂地分享,並結合實際案例。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
Class檔案結構簡介
Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序準確地排列在Class檔案中,中間沒有任何分隔符。當遇到8位位元組以上的資料時,就按照高位在前的方式(最高位位元組在地址最低位、最低位位元組在地址最高位的順序儲存)分割成多個8位位元組儲存。
Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料的,這種偽結構有兩種資料型別:無符號數和表。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
無符號數用u1、u2、u4、u8分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,可以用來描述數字、索引引用、數量值或者UTF-8編碼構成的字串值。
表是由多個無符號數或其他表作為資料項構成的複合資料型別,所有的表都習慣地以“_info”結尾。表的資料結構和樹很類似,無符號數相當於它的葉子節點,其他的表相當於它的子節點。整個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 | interface_count | 介面索引 |
u2 | fields_count | 1 | 欄位計數值 |
field_info | fields | fields_count | 欄位 |
u2 | methods_count | 1 | 方法計數值 |
method_info | fields | methods_count | 方法 |
u2 | attributes_count | 1 | 屬性計數值 |
attribute_info | attributes | attributes_count | 屬性 |
可以發現,無論是無符號數還是表,當需要描述同一種類型又數量不定的多條資料時,就會用一個前置的計數器加幾個連續的資料項的方式,這個時候我們就把這種一系列連續的某種型別的資料叫做這個型別的集合。
在Class檔案中,無論是順序還是數量,甚至是資料儲存的位元組序,都必須嚴格按照上面表格進行設定,哪個位元組代表什麼含義,長度是多少,先後順序怎麼樣,都不允許改變。接下來看一下各個資料項的具體含義。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
魔數
魔數(Magic Number)是每個Class檔案的前4個位元組,它用來確定當前檔案是否是一個被Java虛擬機器所接受的Class檔案。很多檔案儲存標準中都使用了魔數進行身份識別,比如gif、jpeg等圖片檔案中都有魔數。使用魔數而不使用副檔名是出於安全考慮,因為副檔名更容易被修改。檔案格式制定者可以自主選擇魔數,只要這個魔數沒有被廣泛使用又不和其他檔案混淆就可以。
Class檔案的魔數是:0xCAFEBABE(咖啡寶貝?),這個魔數在Java還被稱為“Oak”語言的時候(大概是1991年)就確定下來了,據Java開發小組最初的關鍵成員Patrick Naughton說:“我們一直在尋找一些好玩的、容易記憶的東西,選擇0xCAFEBABE是因為它象徵著著名咖啡品牌Peet's Coffee中深受歡迎的Baristas咖啡”,他們是真的很喜歡喝咖啡啊,可能也預示著日後“Java”這個名字的出現。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
為了更快的理解,我準備了一個實際案例,一段非常簡單的Java程式碼:
public class OneMoreStudy {
private int number;
private int plusOne() {
return number + 1;
}
}
使用JDK 1.7把這段程式碼編譯成Class檔案,用HexEd開啟,就可以到魔數了,如下圖:
在接下來的分享中,也會經常使用這個Class檔案。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
次版本號和主版本號
緊跟著魔數的第5和第6個位元組是次版本號(Minor Version),第7和第8個位元組是主版本號(Major Version)。Java的主版本號是從45開始的,從JDK 1.1以後每個JDK大版本釋出主版本號都加1,高版本的JDK向下相容低版本的Class檔案,但不能執行更高版本的Class檔案,即使Class檔案的格式沒有發生任何變化,Java虛擬機器也會拒絕執行超過其版本號的Class檔案。
再來看一下之前的Class檔案例子:
表示次版本號的第5和第6個位元組值為0x0000,表示主版本號的第7和第8個位元組值為0x0033,也就是十進位制的51,說明這個Class檔案可以被JDK 1.7及其以上版本的Java虛擬機器執行。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
常量池
緊跟著主版本號的就是常量池,它可以理解為Class檔案的資源倉庫,也是Class檔案結構中與其他資料項關聯最多的資料型別。因為在常量池中的常量數量是不固定的,所以首先有一個u2型別的資料,表示常量池容量大小(constant_pool_count)。
常量池的容量計數不是從0開始的,而是從1開始的,這是因為0有它的特殊用用途,那就是為了表達在特殊情況下需要表達“不引用任何一個常量池專案”的含義。在Class檔案結構中只有常量池的容量計數是從1開始的,對於其他集合,包括介面索引集合、欄位集合、方法集合等的容量計數都是從0開始的。
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
再來看一下之前的Class檔案例子:
常量池容器計數值為0x0013,也就是十進位制的19,它表示常量池中有18個常量,索引值範圍從1到18。
常量池中主要儲存兩種常量:字面量(Literal)和符號引用(Symbolic References)。字面量比較接近Java語言層面的常量,比如文字字串、宣告為final的常量值。符號引用則是編譯原理層次的概念,它包括以下三種:
- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
常量池中每一個常量都是一個表,共有14種不同的常量型別(JDK1.7及之前版本),每一種型別的表在第一位都有一個u1型別的標誌位,具體如下表:
型別 | 標誌位 | 描述 |
---|---|---|
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_InvokeDynamic_info | 18 | 表示一個動態方法呼叫點 |
有個一個專門分析Class檔案位元組碼的工具javap,我們用它直接看一下之前的Class檔案例子裡的18個常量(常量池以外的資訊已省略):
E:\>javap -verbose OneMoreStudy
Compiled from "OneMoreStudy.java"
minor version: 0
major version: 51
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // OneMoreStudy.number:I
#3 = Class #17 // OneMoreStudy
#4 = Class #18 // java/lang/Object
#5 = Utf8 number
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 plusOne
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 OneMoreStudy.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // number:I
#17 = Utf8 OneMoreStudy
#18 = Utf8 java/lang/Object
其中,有一些常量好像在程式碼裡沒有出現過,如“I”、“
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
訪問標誌
緊跟著常量池的2個位元組表示訪問標誌(access_flags),它用於識別一些類或介面層次的訪問資訊,具體見下表:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
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 | 是否是列舉 |
其中,ACC_SUPER在JDK 1.0.2之後編譯出來的Class檔案必須為true;ACC_ABSTRACT對於介面或抽象類來說為true,其他類為false。
之前的例子OneMoreStudy是一個普通的類,不是介面、註解或列舉,只被public修飾,沒有被宣告為final或abstract,而且是JDK 1.7編譯的,所以只有ACC_PUBLIC和ACC_SUPER為true,所以它的訪問標誌應該是0x0001 | 0x0020 = 0x0021,如下圖:
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。
下回分解
由於篇幅限制,這次的分享先暫時到這裡,希望大家更好地消化吸收。欲知後事如何,請聽下回分解!敬請期待!
歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨