什么是Class文件唠粥?
在Java剛剛誕生的時(shí)候就提出了一個(gè)非常著名的口號(hào):“一次編寫胡嘿,到處運(yùn)行蛉艾。(Write Once,Run Anywhere)”衷敌。為了實(shí)現(xiàn)平臺(tái)無(wú)關(guān)性勿侯,各種不同平臺(tái)的虛擬機(jī)都統(tǒng)一使用一種程序儲(chǔ)存格式,就是字節(jié)碼(ByteCode)缴罗。它就以二進(jìn)制字節(jié)流的方式被存放在Class文件中助琐,其中包含了Java虛擬機(jī)指令集和符號(hào)表以及其他輔助信息。
為什么需要了解Class文件結(jié)構(gòu)面氓?
一般對(duì)于數(shù)據(jù)結(jié)構(gòu)的分享難免比較枯燥兵钮,但是了解Class文件結(jié)構(gòu)是了解Java虛擬機(jī)的重要基礎(chǔ)之一。如果想比較深入地了解Java虛擬機(jī)舌界,那么Class文件結(jié)構(gòu)是不能不接觸的掘譬。我會(huì)力求在保證邏輯準(zhǔn)確的基礎(chǔ)上,盡量通俗易懂地分享呻拌,并結(jié)合實(shí)際案例葱轩。
Class文件結(jié)構(gòu)簡(jiǎn)介
Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序準(zhǔn)確地排列在Class文件中藐握,中間沒有任何分隔符靴拱。當(dāng)遇到8位字節(jié)以上的數(shù)據(jù)時(shí),就按照高位在前的方式(最高位字節(jié)在地址最低位猾普、最低位字節(jié)在地址最高位的順序儲(chǔ)存)分割成多個(gè)8位字節(jié)儲(chǔ)存缭嫡。
Class文件格式采用一種類似于C語(yǔ)言結(jié)構(gòu)體的偽結(jié)構(gòu)來(lái)儲(chǔ)存數(shù)據(jù)的,這種偽結(jié)構(gòu)有兩種數(shù)據(jù)類型:無(wú)符號(hào)數(shù)和表抬闷。
無(wú)符號(hào)數(shù)用u1、u2耕突、u4笤成、u8分別代表1個(gè)字節(jié)、2個(gè)字節(jié)眷茁、4個(gè)字節(jié)和8個(gè)字節(jié)的無(wú)符號(hào)數(shù)炕泳,可以用來(lái)描述數(shù)字、索引引用上祈、數(shù)量值或者UTF-8編碼構(gòu)成的字符串值培遵。
表是由多個(gè)無(wú)符號(hào)數(shù)或其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型浙芙,所有的表都習(xí)慣地以“_info”結(jié)尾。表的數(shù)據(jù)結(jié)構(gòu)和樹很類似籽腕,無(wú)符號(hào)數(shù)相當(dāng)于它的葉子節(jié)點(diǎn)嗡呼,其他的表相當(dāng)于它的子節(jié)點(diǎn)。整個(gè)Class文件就本質(zhì)上也是一個(gè)表皇耗,具體結(jié)構(gòu)如下:
可以發(fā)現(xiàn)南窗,無(wú)論是無(wú)符號(hào)數(shù)還是表,當(dāng)需要描述同一種類型又?jǐn)?shù)量不定的多條數(shù)據(jù)時(shí)郎楼,就會(huì)用一個(gè)前置的計(jì)數(shù)器加幾個(gè)連續(xù)的數(shù)據(jù)項(xiàng)的方式万伤,這個(gè)時(shí)候我們就把這種一系列連續(xù)的某種類型的數(shù)據(jù)叫做這個(gè)類型的集合。
在Class文件中呜袁,無(wú)論是順序還是數(shù)量敌买,甚至是數(shù)據(jù)存儲(chǔ)的字節(jié)序,都必須嚴(yán)格按照上面表格進(jìn)行設(shè)定阶界,哪個(gè)字節(jié)代表什么含義虹钮,長(zhǎng)度是多少,先后順序怎么樣荐操,都不允許改變芜抒。接下來(lái)看一下各個(gè)數(shù)據(jù)項(xiàng)的具體含義。
魔數(shù)
魔數(shù)(Magic Number)是每個(gè)Class文件的前4個(gè)字節(jié)托启,它用來(lái)確定當(dāng)前文件是否是一個(gè)被Java虛擬機(jī)所接受的Class文件宅倒。很多文件存儲(chǔ)標(biāo)準(zhǔn)中都使用了魔數(shù)進(jìn)行身份識(shí)別,比如gif屯耸、jpeg等圖片文件中都有魔數(shù)拐迁。使用魔數(shù)而不使用擴(kuò)展名是出于安全考慮,因?yàn)閿U(kuò)展名更容易被修改疗绣。文件格式制定者可以自主選擇魔數(shù)线召,只要這個(gè)魔數(shù)沒有被廣泛使用又不和其他文件混淆就可以。
Class文件的魔數(shù)是:0xCAFEBABE多矮,這個(gè)魔數(shù)在Java還被稱為“Oak”語(yǔ)言的時(shí)候就確定下來(lái)了缓淹,據(jù)Java開發(fā)小組最初的關(guān)鍵成員Patrick Naughton說(shuō):“我們一直在尋找一些好玩的、容易記憶的東西塔逃,選擇0xCAFEBABE是因?yàn)樗笳髦Х绕放芇eet's Coffee中深受歡迎的Baristas咖啡”讯壶,他們是真的很喜歡喝咖啡啊,可能也預(yù)示著日后“Java”這個(gè)名字的出現(xiàn)湾盗。
為了更快的理解伏蚊,我準(zhǔn)備了一個(gè)實(shí)際案例,一段非常簡(jiǎn)單的Java代碼:
public class OneMoreStudy {
private int number;
private int plusOne() {
return number + 1;
}
}
使用JDK 1.7把這段代碼編譯成Class文件格粪,用HexEd打開躏吊,就可以到魔數(shù)了氛改,如下圖:
在接下來(lái)的分享中,也會(huì)經(jīng)常使用這個(gè)Class文件比伏。
次版本號(hào)和主版本號(hào) 緊跟著魔數(shù)的第5和第6個(gè)字節(jié)是次版本號(hào)(Minor Version)胜卤,第7和第8個(gè)字節(jié)是主版本號(hào)(Major Version)。Java的主版本號(hào)是從45開始的凳怨,從JDK 1.1以后每個(gè)JDK大版本發(fā)布主版本號(hào)都加1瑰艘,高版本的JDK向下兼容低版本的Class文件,但不能運(yùn)行更高版本的Class文件肤舞,即使Class文件的格式?jīng)]有發(fā)生任何變化紫新,Java虛擬機(jī)也會(huì)拒絕運(yùn)行超過(guò)其版本號(hào)的Class文件。
再來(lái)看一下之前的Class文件例子:
表示次版本號(hào)的第5和第6個(gè)字節(jié)值為0x0000李剖,表示主版本號(hào)的第7和第8個(gè)字節(jié)值為0x0033芒率,也就是十進(jìn)制的51,說(shuō)明這個(gè)Class文件可以被JDK 1.7及其以上版本的Java虛擬機(jī)運(yùn)行篙顺。
常量池 緊跟著主版本號(hào)的就是常量池偶芍,它可以理解為Class文件的資源倉(cāng)庫(kù),也是Class文件結(jié)構(gòu)中與其他數(shù)據(jù)項(xiàng)關(guān)聯(lián)最多的數(shù)據(jù)類型德玫。因?yàn)樵诔A砍刂械某A繑?shù)量是不固定的匪蟀,所以首先有一個(gè)u2類型的數(shù)據(jù),表示常量池容量大性咨(constant_pool_count)材彪。
常量池的容量計(jì)數(shù)不是從0開始的,而是從1開始的琴儿,這是因?yàn)?有它的特殊用用途段化,那就是為了表達(dá)在特殊情況下需要表達(dá)“不引用任何一個(gè)常量池項(xiàng)目”的含義。在Class文件結(jié)構(gòu)中只有常量池的容量計(jì)數(shù)是從1開始的造成,對(duì)于其他集合显熏,包括接口索引集合、字段集合晒屎、方法集合等的容量計(jì)數(shù)都是從0開始的喘蟆。
再來(lái)看一下之前的Class文件例子:
常量池容器計(jì)數(shù)值為0x0013,也就是十進(jìn)制的19鼓鲁,它表示常量池中有18個(gè)常量履肃,索引值范圍從1到18。
常量池中主要存儲(chǔ)兩種常量:字面量(Literal)和符號(hào)引用(Symbolic References)坐桩。字面量比較接近Java語(yǔ)言層面的常量,比如文本字符串封锉、聲明為final的常量值绵跷。符號(hào)引用則是編譯原理層次的概念膘螟,它包括以下三種:
1.類和接口的全限定名
2.字段的名稱和描述符
3.方法的名稱和描述符
常量池中每一個(gè)常量都是一個(gè)表,共有14種不同的常量類型(JDK1.7及之前版本)碾局,每一種類型的表在第一位都有一個(gè)u1類型的標(biāo)志位荆残,具體如下表:
有個(gè)一個(gè)專門分析Class文件字節(jié)碼的工具javap,我們用它直接看一下之前的Class文件例子里的18個(gè)常量(常量池以外的信息已省略):
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)過(guò)内斯,如“I”、“”像啼、“Code”俘闯、“LineNumberTable”、“SourceFile”忽冻。它們其實(shí)自動(dòng)生成的真朗,是后面要分享的字段表、方法表僧诚、屬性表引用到的遮婶,用于描述一些不方便使用“固定字節(jié)”進(jìn)行表達(dá)的內(nèi)容。
訪問(wèn)標(biāo)志 緊跟著常量池的2個(gè)字節(jié)表示訪問(wèn)標(biāo)志(access_flags)湖笨,它用于識(shí)別一些類或接口層次的訪問(wèn)信息旗扑,具體見下表:
其中,ACC_SUPER在JDK 1.0.2之后編譯出來(lái)的Class文件必須為true慈省;ACC_ABSTRACT對(duì)于接口或抽象類來(lái)說(shuō)為true臀防,其他類為false。
之前的例子OneMoreStudy是一個(gè)普通的類辫呻,不是接口清钥、注解或枚舉,只被public修飾放闺,沒有被聲明為final或abstract祟昭,而且是JDK 1.7編譯的,所以只有ACC_PUBLIC和ACC_SUPER為true怖侦,所以它的訪問(wèn)標(biāo)志應(yīng)該是0x0001 | 0x0020 = 0x0021篡悟,如下圖: