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文件,用打開,就可以到魔數了,如下圖:
在接下來的分享中,也會經常使用這個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”、“ ”、“Code”、“LineNumberTable”、“SourceFile”。它們其實自動生成的,是後面要分享的字段表、方法表、屬性表引用到的,用於描述一些不方便使用“固定字節”進行表達的內容。
歡迎關注微信公眾號:萬貓學社,每周一分享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技術乾貨。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※專營大陸空運台灣貨物推薦
※台灣空運大陸一條龍服務
※評比南投搬家公司費用收費行情懶人包大公開
※南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!