什么是Class文件?
在Java剛剛誕生的時候就提出了一個非常著名的口號:“一次編寫,到處運(yùn)行。(Write Once,Run Anywhere)”。為了實現(xiàn)平臺無關(guān)性,各種不同平臺的虛擬機(jī)都統(tǒng)一使用一種程序儲存格式,就是字節(jié)碼(ByteCode)。它就以二進(jìn)制字節(jié)流的方式被存放在Class文件中,其中包含了Java虛擬機(jī)指令集和符號表以及其他輔助信息。
為什么需要了解Class文件結(jié)構(gòu)?
一般對于數(shù)據(jù)結(jié)構(gòu)的分享難免比較枯燥,但是了解Class文件結(jié)構(gòu)是了解Java虛擬機(jī)的重要基礎(chǔ)之一。如果想比較深入地了解Java虛擬機(jī),那么Class文件結(jié)構(gòu)是不能不接觸的。我會力求在保證邏輯準(zhǔn)確的基礎(chǔ)上,盡量通俗易懂地分享,并結(jié)合實際案例。
Class文件結(jié)構(gòu)簡介
Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個數(shù)據(jù)項目嚴(yán)格按照順序準(zhǔn)確地排列在Class文件中,中間沒有任何分隔符。當(dāng)遇到8位字節(jié)以上的數(shù)據(jù)時,就按照高位在前的方式(最高位字節(jié)在地址最低位、最低位字節(jié)在地址最高位的順序儲存)分割成多個8位字節(jié)儲存。
Class文件格式采用一種類似于C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來儲存數(shù)據(jù)的,這種偽結(jié)構(gòu)有兩種數(shù)據(jù)類型:無符號數(shù)和表。
無符號數(shù)用u1、u2、u4、u8分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù),可以用來描述數(shù)字、索引引用、數(shù)量值或者UTF-8編碼構(gòu)成的字符串值。
表是由多個無符號數(shù)或其他表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型,所有的表都習(xí)慣地以“_info”結(jié)尾。表的數(shù)據(jù)結(jié)構(gòu)和樹很類似,無符號數(shù)相當(dāng)于它的葉子節(jié)點,其他的表相當(dāng)于它的子節(jié)點。整個Class文件就本質(zhì)上也是一個表,具體結(jié)構(gòu)如下:

可以發(fā)現(xiàn),無論是無符號數(shù)還是表,當(dāng)需要描述同一種類型又?jǐn)?shù)量不定的多條數(shù)據(jù)時,就會用一個前置的計數(shù)器加幾個連續(xù)的數(shù)據(jù)項的方式,這個時候我們就把這種一系列連續(xù)的某種類型的數(shù)據(jù)叫做這個類型的集合。
在Class文件中,無論是順序還是數(shù)量,甚至是數(shù)據(jù)存儲的字節(jié)序,都必須嚴(yán)格按照上面表格進(jìn)行設(shè)定,哪個字節(jié)代表什么含義,長度是多少,先后順序怎么樣,都不允許改變。接下來看一下各個數(shù)據(jù)項的具體含義。
魔數(shù)
魔數(shù)(Magic Number)是每個Class文件的前4個字節(jié),它用來確定當(dāng)前文件是否是一個被Java虛擬機(jī)所接受的Class文件。很多文件存儲標(biāo)準(zhǔn)中都使用了魔數(shù)進(jìn)行身份識別,比如gif、jpeg等圖片文件中都有魔數(shù)。使用魔數(shù)而不使用擴(kuò)展名是出于安全考慮,因為擴(kuò)展名更容易被修改。文件格式制定者可以自主選擇魔數(shù),只要這個魔數(shù)沒有被廣泛使用又不和其他文件混淆就可以。
Class文件的魔數(shù)是:0xCAFEBABE,這個魔數(shù)在Java還被稱為“Oak”語言的時候就確定下來了,據(jù)Java開發(fā)小組最初的關(guān)鍵成員Patrick Naughton說:“我們一直在尋找一些好玩的、容易記憶的東西,選擇0xCAFEBABE是因為它象征著著名咖啡品牌Peet's Coffee中深受歡迎的Baristas咖啡”,他們是真的很喜歡喝咖啡啊,可能也預(yù)示著日后“Java”這個名字的出現(xiàn)。
為了更快的理解,我準(zhǔn)備了一個實際案例,一段非常簡單的Java代碼:
public class OneMoreStudy {
private int number;
private int plusOne() {
return number + 1;
}
}
使用JDK 1.7把這段代碼編譯成Class文件,用HexEd打開,就可以到魔數(shù)了,如下圖:

在接下來的分享中,也會經(jīng)常使用這個Class文件。
次版本號和主版本號 緊跟著魔數(shù)的第5和第6個字節(jié)是次版本號(Minor Version),第7和第8個字節(jié)是主版本號(Major Version)。Java的主版本號是從45開始的,從JDK 1.1以后每個JDK大版本發(fā)布主版本號都加1,高版本的JDK向下兼容低版本的Class文件,但不能運(yùn)行更高版本的Class文件,即使Class文件的格式?jīng)]有發(fā)生任何變化,Java虛擬機(jī)也會拒絕運(yùn)行超過其版本號的Class文件。
再來看一下之前的Class文件例子:

表示次版本號的第5和第6個字節(jié)值為0x0000,表示主版本號的第7和第8個字節(jié)值為0x0033,也就是十進(jìn)制的51,說明這個Class文件可以被JDK 1.7及其以上版本的Java虛擬機(jī)運(yùn)行。
常量池 緊跟著主版本號的就是常量池,它可以理解為Class文件的資源倉庫,也是Class文件結(jié)構(gòu)中與其他數(shù)據(jù)項關(guān)聯(lián)最多的數(shù)據(jù)類型。因為在常量池中的常量數(shù)量是不固定的,所以首先有一個u2類型的數(shù)據(jù),表示常量池容量大?。╟onstant_pool_count)。
常量池的容量計數(shù)不是從0開始的,而是從1開始的,這是因為0有它的特殊用用途,那就是為了表達(dá)在特殊情況下需要表達(dá)“不引用任何一個常量池項目”的含義。在Class文件結(jié)構(gòu)中只有常量池的容量計數(shù)是從1開始的,對于其他集合,包括接口索引集合、字段集合、方法集合等的容量計數(shù)都是從0開始的。
再來看一下之前的Class文件例子:

常量池容器計數(shù)值為0x0013,也就是十進(jìn)制的19,它表示常量池中有18個常量,索引值范圍從1到18。
常量池中主要存儲兩種常量:字面量(Literal)和符號引用(Symbolic References)。字面量比較接近Java語言層面的常量,比如文本字符串、聲明為final的常量值。符號引用則是編譯原理層次的概念,它包括以下三種:
1.類和接口的全限定名
2.字段的名稱和描述符
3.方法的名稱和描述符
常量池中每一個常量都是一個表,共有14種不同的常量類型(JDK1.7及之前版本),每一種類型的表在第一位都有一個u1類型的標(biāo)志位,具體如下表:

有個一個專門分析Class文件字節(jié)碼的工具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
其中,有一些常量好像在代碼里沒有出現(xiàn)過,如“I”、“”、“Code”、“LineNumberTable”、“SourceFile”。它們其實自動生成的,是后面要分享的字段表、方法表、屬性表引用到的,用于描述一些不方便使用“固定字節(jié)”進(jìn)行表達(dá)的內(nèi)容。
訪問標(biāo)志 緊跟著常量池的2個字節(jié)表示訪問標(biāo)志(access_flags),它用于識別一些類或接口層次的訪問信息,具體見下表:

其中,ACC_SUPER在JDK 1.0.2之后編譯出來的Class文件必須為true;ACC_ABSTRACT對于接口或抽象類來說為true,其他類為false。
之前的例子OneMoreStudy是一個普通的類,不是接口、注解或枚舉,只被public修飾,沒有被聲明為final或abstract,而且是JDK 1.7編譯的,所以只有ACC_PUBLIC和ACC_SUPER為true,所以它的訪問標(biāo)志應(yīng)該是0x0001 | 0x0020 = 0x0021,如下圖:
