一般對于資料結構的分享難免比較枯燥,但是了解Class檔案結構是了解Java虛拟機的重要基礎之一。如果想比較深入地了解Java虛拟機,那麼Class檔案結構是不能不接觸的。我會力求在保證邏輯準确的基礎上,盡量通俗易懂地分享,并結合實際案例。
什麼是Class檔案?
在Java剛剛誕生的時候就提出了一個非常著名的口号:“一次編寫,到處運作。(Write Once,Run Anywhere)”。為了實作平台無關性,各種不同平台的虛拟機都統一使用一種程式儲存格式,就是位元組碼(ByteCode)。它就以二進制位元組流的方式被存放在Class檔案中,其中包含了Java虛拟機指令集和符号表以及其他輔助資訊。
歡迎關注微信公衆号:
萬貓學社,每周一分享Java技術幹貨。
為什麼需要了解Class檔案結構?
Class檔案結構簡介
Class檔案是一組以8位位元組為基礎機關的二進制流,各個資料項目嚴格按照順序準确地排列在Class檔案中,中間沒有任何分隔符。當遇到8位位元組以上的資料時,就按照高位在前的方式(最高位位元組在位址最低位、最低位位元組在位址最高位的順序儲存)分割成多個8位位元組儲存。
Class檔案格式采用一種類似于C語言結構體的僞結構來儲存資料的,這種僞結構有兩種資料類型:無符号數和表。
無符号數用u1、u2、u4、u8分别代表1個位元組、2個位元組、4個位元組和8個位元組的無符号數,可以用來描述數字、索引引用、數量值或者UTF-8編碼構成的字元串值。
表是由多個無符号數或其他表作為資料項構成的複合資料類型,所有的表都習慣地以“_info”結尾。表的資料結構和樹很類似,無符号數相當于它的葉子節點,其他的表相當于它的子節點。整個Class檔案就本質上也是一個表,具體結構如下:
類型 | 名稱 | 數量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔數 |
u2 | minor_version | 次版本号 | |
major_version | 主版本号 | ||
constant_pool_count | 常量池容量計數值 | ||
cp_info | constant_pool | constant_pool_count - 1 | 常量池 |
access_flags | 通路标志 | ||
this_class | 類索引 | ||
super_class | 父類索引 | ||
interfaces_count | 接口索引計數值 | ||
interfaces | interface_count | 接口索引 | |
fields_count | 字段計數值 | ||
field_info | fields | 字段 | |
methods_count | 方法計數值 | ||
method_info | 方法 | ||
attributes_count | 屬性計數值 | ||
attribute_info | attributes | 屬性 |
可以發現,無論是無符号數還是表,當需要描述同一種類型又數量不定的多條資料時,就會用一個前置的計數器加幾個連續的資料項的方式,這個時候我們就把這種一系列連續的某種類型的資料叫做這個類型的集合。
在Class檔案中,無論是順序還是數量,甚至是資料存儲的位元組序,都必須嚴格按照上面表格進行設定,哪個位元組代表什麼含義,長度是多少,先後順序怎麼樣,都不允許改變。接下來看一下各個資料項的具體含義。
魔數(Magic Number)是每個Class檔案的前4個位元組,它用來确定目前檔案是否是一個被Java虛拟機所接受的Class檔案。很多檔案存儲标準中都使用了魔數進行身份識别,比如gif、jpeg等圖檔檔案中都有魔數。使用魔數而不使用擴充名是出于安全考慮,因為擴充名更容易被修改。檔案格式制定者可以自主選擇魔數,隻要這個魔數沒有被廣泛使用又不和其他檔案混淆就可以。
Class檔案的魔數是:0xCAFEBABE(咖啡寶貝?),這個魔數在Java還被稱為“Oak”語言的時候(大概是1991年)就确定下來了,據Java開發小組最初的關鍵成員Patrick Naughton說:“我們一直在尋找一些好玩的、容易記憶的東西,選擇0xCAFEBABE是因為它象征着著名咖啡品牌Peet's Coffee中深受歡迎的Baristas咖啡”,他們是真的很喜歡喝咖啡啊,可能也預示着日後“Java”這個名字的出現。
為了更快的了解,我準備了一個實際案例,一段非常簡單的Java代碼:
public class OneMoreStudy {
private int number;
private int plusOne() {
return number + 1;
}
}
使用JDK 1.7把這段代碼編譯成Class檔案,用HexEd打開,就可以到魔數了,如下圖:
在接下來的分享中,也會經常使用這個Class檔案。
次版本号和主版本号
緊跟着魔數的第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虛拟機運作。
緊跟着主版本号的就是常量池,它可以了解為Class檔案的資源倉庫,也是Class檔案結構中與其他資料項關聯最多的資料類型。因為在常量池中的常量數量是不固定的,是以首先有一個u2類型的資料,表示常量池容量大小(constant_pool_count)。
常量池的容量計數不是從0開始的,而是從1開始的,這是因為0有它的特殊用用途,那就是為了表達在特殊情況下需要表達“不引用任何一個常量池項目”的含義。在Class檔案結構中隻有常量池的容量計數是從1開始的,對于其他集合,包括接口索引集合、字段集合、方法集合等的容量計數都是從0開始的。
常量池容器計數值為0x0013,也就是十進制的19,它表示常量池中有18個常量,索引值範圍從1到18。
常量池中主要存儲兩種常量:字面量(Literal)和符号引用(Symbolic References)。字面量比較接近Java語言層面的常量,比如文本字元串、聲明為final的常量值。符号引用則是編譯原理層次的概念,它包括以下三種:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
常量池中每一個常量都是一個表,共有14種不同的常量類型(JDK1.7及之前版本),每一種類型的表在第一位都有一個u1類型的标志位,具體如下表:
标志位 | ||
---|---|---|
CONSTANT_Utf8_info | 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”、“”、“Code”、“LineNumberTable”、“SourceFile”。它們其實自動生成的,是後面要分享的字段表、方法表、屬性表引用到的,用于描述一些不友善使用“固定位元組”進行表達的内容。
緊跟着常量池的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,如下圖:
下回分解
由于篇幅限制,這次的分享先暫時到這裡,希望大家更好地消化吸收。欲知後事如何,請聽下回分解!敬請期待!
作者:萬貓學社
出處:http://www.cnblogs.com/heihaozi/
版權聲明:本文遵循 CC 4.0 BY-NC-SA 版權協定,轉載請附上原文出處連結和本聲明。
微信掃描二維碼,關注
,回複「
電子書」,免費擷取12本Java必讀技術書籍。