一、概述
各種不同平臺的Java虛擬機器, 以及所有平臺都統一支援的程式儲存格式——位元組碼(Byte Code)是構成平臺無關性的基石,所以class檔案主要用於解決平臺無關性的中間檔案。如下圖所示:
java虛擬機器不與包括Java語言在內的任何程式語言繫結, 它只與“Class檔案”這種特定的二進位制檔案格式所關聯, Class檔案中包含了Java虛擬機器指令集、 符號表以及若干其他輔助資訊。
每一個class檔案都對應著唯一一個類或者介面的定義資訊,但是相對地,類或者介面並不一定都必須定義在檔案裡(比如類或者介面也可以通過類載入器直接生成)
每個class檔案都是由位元組流組成,各個資料專案嚴格按照順序緊湊地排列在檔案之中, 中間沒有新增任何分隔符,每個位元組流含有8個二進位制位,所有的16位,32位和64位長度的資料將通過2個,4個和8個連續的8位位元組來對其進行表示,多位元組資料總是按照big-endian(大端在前:也就是說高位位元組儲存在低的地址上面,而低位位元組儲存到高地址上面)的順序進行儲存,在Java JDK中,可以使用java.io.DataInput、java.io.DataOutput等介面和java.io.DataInputStream和java.io.DataOutputStream等類來訪問這種格式的資料Class檔案結構採用類似C語言的結構體來儲存資料的。
Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料,主要有兩類資料項,無符號數和表,無符號數用來表述數字,索引引用以及字串等,比如 u1,u2,u4,u8分別代表1個位元組,2個位元組,4個位元組,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 | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.1、魔數和java版本號
每個Class檔案的頭4個位元組被稱為魔數(Magic Number) , 它的唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。Class檔案的魔數取得很有“浪漫氣息”,
值為0xCAFEBABE(咖啡寶貝? )
緊接著魔數的4個位元組儲存的是Class檔案的版本號: 第5和第6個位元組是次版本號(MinorVersion) , 第7和第8個位元組是主版本號(Major Version)
這裡我們使用一個簡單的程式碼進行分析:
public class TestClass { private int m; public int inc() { return m + 1; } }
使用javac命令對其進行編譯,並使用WinHex (下載地址:http://www.x-ways.net/winhex/index-m.html)工具開啟,得到如下的圖,前面幾位就是魔數和版本號
這裡可以得出我們使用的版本為java1.8,16進位制的34等於10進位制的52
2.2、常量池
緊接著主、 次版本號之後的是常量池入口, 常量池可以比喻為Class檔案裡的資源倉庫, 它是Class檔案結構中與其他專案關聯最多的資料, 通常也是佔用Class檔案空間最大的資料專案之一, 另外, 它還是在Class檔案中第一個出現的表型別資料專案 ,常量池的入口需要放置一項u2型別的資料, 代表常量池容量計數值(constant_pool_count) ,這個容量計數是從1開始的。如下圖所示:常量池容量(偏移地址: 0x00000008) 為十六進位制數0x0013,則十進位制為19,則這裡有18個長常量,索引範圍為1-18,在Class檔案格式規範制定之時, 設計者將第0項常量空出來是有特殊考慮的, 這樣做的目的在於, 如果後面某些指向常量池的索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的含義, 可以把索引值設定為0來表示。
然後我們使用javap命令檢視該class檔案:(這裡明顯顯示為18個常量)
常量池中主要存放兩大類常量: 字面量(Literal) 和符號引用(Symbolic References) 。
字面量比較接近於Java語言層面的常量概念, 如文字字串、 被宣告為final的常量值等。
符號引用則屬於編譯原理方面的概念, 主要包括下面幾類常量:
- 被模組匯出或者開放的包(Package)
- 類和介面的全限定名(Fully Qualified Name)
- 欄位的名稱和描述符(Descriptor)
- 方法的名稱和描述符
- 方法控制程式碼和方法型別(Method Handle、 Method Type、 Invoke Dynamic)
- 動態呼叫點和動態常量(Dynamically-Computed Call Site、 Dynamically-Computed Constant)
虛擬機器在載入Class檔案時才會進行動態連線,也就是說,Class檔案中不會儲存各個方法、 欄位最終在記憶體中的佈局資訊, 這些欄位、 方法的符號引用不經過虛擬機器在執行期轉換的話是無法得到真正的記憶體入口地址, 也就無法直接被虛擬機器使用的,當虛擬機器做類載入時, 將會從常量池獲得對應的符號引用, 再在類建立時或執行時解析、 翻譯到具體的記憶體地址之中常量池中每一項常量都是一個表,截至JDK13, 常量表中分別有17種不同型別的常量。這17類表都有一個共同的特點, 表結構起始的第一位是個u1型別的標誌位,代表著當前常量屬於哪種常量型別。 17種常量型別所代表的具體含義如下圖所示。
符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到了記憶體中。
直接引用:直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經存在於記憶體之中了
型別 |
專案 |
型別 |
描述 |
CONSTANT_Utf8_info |
tag |
u1 |
值為1 |
length |
u2 |
utf-8縮略編碼字串佔用位元組數 |
|
bytes |
u1 |
長度為length的utf-8縮略編碼字串 |
|
CONSTANT_Integer_info |
tag |
u1 |
值為3 |
bytes |
u4 |
按照高位在前儲存的int值 |
|
CONSTANT_Float_info |
tag |
u1 |
值為4 |
bytes |
u4 |
按照高位在前儲存的float值 |
|
CONSTANT_Long_info |
tag |
u1 |
值為5 |
bytes |
u8 |
按照高位在前儲存的long值 |
|
CONSTANT_Double_info |
tag |
u1 |
值為6 |
bytes |
u8 |
按照高位在前儲存的double值 |
|
CONSTANT_Class_info |
tag |
u1 |
值為7 |
index |
u2 |
指向全限定名常量項的索引 |
|
CONSTANT_String_info |
tag |
u1 |
值為8 |
index |
u2 |
指向字串字面量的索引 |
|
CONSTANT_Fieldref_info |
tag |
u1 |
值為9 |
index |
u2 |
指向宣告欄位的類或介面描述符CONSTANT_Class_info的索引項 |
|
index |
u2 |
指向欄位描述符CONSTANT_NameAndType_info的索引項 |
|
CONSTANT_Methodref_info |
tag |
u1 |
值為10 |
index |
u2 |
指向宣告方法的類描述符CONSTANT_Class_info的索引項 |
|
index |
u2 |
指向名稱及型別描述符CONSTANT_NameAndType_info的索引項 |
|
CONSTANT_InterfaceMethodref_info |
tag |
u1 |
值為11 |
index |
u2 |
指向宣告方法的介面描述符CONSTANT_Class_info的索引項 |
|
index |
u2 |
指向名稱及型別描述符CONSTANT_NameAndType_info的索引項 |
|
CONSTANT_NameAndType_info
|
tag |
u1 |
值為12 |
index |
u2 |
指向該欄位或方法名稱常量項的索引 |
|
index |
u2 |
指向該欄位或方法描述符常量項的索引 |
|
CONSTANT_MethodHandle_info |
tag |
u1 |
值為15 |
refrence_kind | u1 | 值必須在1-9之間,決定了方法控制程式碼的型別,方法控制程式碼的型別的值表示方法控制程式碼位元組碼的行為 | |
refrence_index | u2 | 值必須是對常量池的有效索引 | |
CONSTANT_MethodType_info | tag | u1 | 值為16 |
descriptor_index | u2 | 值必須對常量池的有效索引,常量池在該處的項必須是CONSTANT_Utf8_info表示方法的描述符 | |
CONSTANT_Dynamic_info | tab | u1 | 值為17 |
bootstrap_method_attr_index | u2 | 值必須對當前Class檔案中引導方法表的bootstrap_methods[]陣列的有效索引 | |
name_and_type_index | u2 | 值必須對當前常量池的有效索引,常量池中在該索引出的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 | |
CONSTANT_InvokeDynamic_info | tag | u1 | 值為18 |
bootstrap_method_attr_index | u2 | 值必須對當前Class檔案中引導方法表的bootstrap_methods[]陣列的有效索引 | |
name_and_type_index | u2 | 值必須對當前常量池的有效索引,常量池中在該索引出的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 | |
CONSTANT_Module_info | tag | u1 | 值為19 |
name_index | u2 | 值必須對常量池的有效索引,常量池在該處的項必須是CONSTANT_Utf8_info表示模組名 | |
CONSTANT_Package_info | tag | u1 | 值為20 |
name_index | u2 | 值必須對常量池的有效索引,常量池在該處的項必須是CONSTANT_Utf8_info表示包名 |
2.3、訪問標誌
在常量池結束之後,緊接著的兩個位元組代表訪問標誌(access_flags),這個標誌用於識別一些類或者介面層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public型別;是否定義為abstract型別,如果是類的話,是否被宣告為final等,具體的標誌位以及標誌的含義如下:
欄位的訪問許可權 |
||
Flag Name |
Value |
Remarks |
ACC_PUBLIC |
0x0001 |
pubilc,包外可訪問。 |
ACC_PRIVATE |
0x0002 |
private,只可在類內訪問。 |
ACC_PROTECTED |
0x0004 |
protected,類內和子類中可訪問。 |
ACC_STATIC |
0x0008 |
static,靜態。 |
ACC_FINAL |
0x0010 |
final,常量。 |
ACC_VOILATIE |
0x0040 |
volatile,直接讀寫記憶體,不可被快取。不可和ACC_FINAL一起使用。 |
ACC_TRANSIENT |
0x0080 |
transient,在序列化中被忽略的欄位。 |
ACC_SYNTHETIC |
0x1000 |
synthetic,由編譯器產生,不存在於原始碼中。 |
ACC_ENUM |
0x4000 |
enum,列舉型別欄位 |
ACC_MODULE |
0x8000 |
標識這是一個模組 |
2.4、類索引、 父類索引與介面索引集合
類索引(this_class) 和父類索引(super_class) 都是一個u2型別的資料, 而介面索引集合(interfaces) 是一組u2型別的資料的集合, Class檔案中由這三項資料來確定該型別的繼承關係。 類索引用於確定這個類的全限定名, 父類索引用於確定這個類的父類的全限定名。 由於Java語言不允許多重繼承, 所以父類索引只有一個, 除了java.lang.Object之外, 所有的Java類都有父類, 因此除了java.lang.Object外, 所有Java類的父類索引都不為0。 介面索引集合就用來描述這個類實現了哪些介面, 這些被實現的介面將按implements關鍵字(如果這個Class檔案表示的是一個介面, 則應當是extends關鍵字) 後的介面順序從左到右排列在介面索引集合中。
2.5、欄位表集合
欄位表(field_info) 用於描述介面或者類中宣告的變數。 Java語言中的“欄位”(Field) 包括類級變數以及例項級變數, 但不包括在方法內部宣告的區域性變數。 欄位可以包括的修飾符有欄位的作用域(public、 private、 protected修飾符) 、 是例項變數還是類變數(static修飾符) 、 可變性(final) 、 併發可見性(volatile修飾符, 是否強制從主記憶體讀寫) 、 可否被序列化(transient修飾符) 、 欄位資料型別(基本型別、 物件、 陣列) 、欄位名稱。 上述這些資訊中, 各個修飾符都是布林值, 要麼有某個修飾符, 要麼沒有, 很適合使用標誌位來表示。 而欄位叫做什麼名字、 欄位被定義為什麼資料型別, 這些都是無法固定的, 只能引用常量池中的常量來描述。 欄位表的最終格式如下。
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
而欄位修飾符放在access_flags專案中, 它與類中的access_flags專案是非常類似的, 都是一個u2的資料型別, 其中可以設定的標誌位和含義如下所示:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 欄位是否為public |
ACC_PRIVATE | 0x0002 | 欄位是否為private |
ACC_PROTECTED | 0x0004 | 欄位是否為protected |
ACC_STATIC | 0x0008 | 欄位是否為static |
ACC_FINAL | 0x0010 | 欄位是否為final |
ACC_SYNCHRONIZED | 0x0020 | 欄位是否為synchronized |
ACC_TRANSIENT | 0x0080 | 欄位是否為transient |
ACC_ABSTRACT | 0x0400 | 欄位是否為abstract |
ACC_SYNTHETIC | 0x1000 | 欄位是否為編譯器自動產生 |
name_index和descriptor_index。 它們都是對常量池項的引用, 分別代表著欄位的簡單名稱以及欄位和方法的描述符。
全限定名:僅僅是把類全名中的“.”替換成了“/”而已,例如類名org.apache.xxxx,器全限定名為org/apache/xxxx。
簡單名稱:就是指沒有型別和引數修飾的方法或者欄位名稱, 比如類中的inc()方法和m欄位的簡單名稱分別就是“inc”和“m”。
方法和欄位的描述符:描述符的作用是用來描述欄位的資料型別、 方法的引數列表(包括數量、 型別以及順序) 和返回值。 根據描述符規則, 基本資料型別(byte、 char、 double、 float、 int、 long、 short、 boolean) 以及代表無返回值的void型別都用一個大寫字元來表示, 而物件型別則用字元L加物件的全限定名來表示,祥見下表:
標識字元 | 含義 |
B | 基本型別byte |
C | 基本型別char |
D | 基本型別double |
F | 基本型別float |
I | 基本型別int |
J | 基本型別long |
S | 基本型別short |
Z | 基本型別boolean |
V | 特殊型別void |
L | 物件型別,如java/lang/Object |
對於陣列型別, 每一維度將使用一個前置的“[”字元來描述, 如一個定義為“java.lang.String[][]”型別的二維陣列將被記錄成“[[Ljava/lang/String; ”, 一個整型陣列“int[]”將被記錄成“[I”
用描述符來描述方法時, 按照先引數列表、 後返回值的順序描述, 引數列表按照引數的嚴格順序放在一組小括號“()”之內。 如方法void inc()的描述符為“()V”, 方法java.lang.String toString()的描述符為“()Ljava/lang/String; ”, 方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target,int targetOffset, int targetCount, int fromIndex)的描述符為“([CII[CIII)I”
2.6、方法表集合
Class檔案儲存格式中對方法的描述與對欄位的描述採用了幾乎完全一致的方式, 方法表的結構如同欄位表一樣, 依次包括訪問標誌(access_flags) 、 名稱索引(name_index) 、 描述符索引(descriptor_index) 、 屬性表集合(attributes) 幾項,如下圖所示
在訪問標誌和屬性表集合的可選項中有所區別,因為volatile關鍵字和transient關鍵字不能修飾方法, 所以方法表的訪問標誌中沒有了ACC_VOLATILE標誌和ACC_TRANSIENT標誌。 與之相對, synchronized、 native、 strictfp和abstract關鍵字可以修飾方法, 方法表的訪問標誌中也相應地增加了ACC_SYNCHRONIZED、ACC_NATIVE、 ACC_STRICTFP和ACC_ABSTRACT標誌。
2.7、屬性表集合
1、code屬性
方法的定義可以通過訪問標誌、 名稱索引、 描述符索引來表達清楚, 但方法裡面的程式碼去哪裡了? 方法裡的Java程式碼, 經過Javac編譯器編譯成位元組碼指令之後, 存放在方法屬性表集合中一個名為“Code”的屬性裡面, 屬性表作為Class檔案格式中最具擴充套件性的一種資料專案,
java程式方法體裡面的程式碼經過Javac編譯器處理之後, 最終變為位元組碼指令儲存在Code屬性內。Code屬性出現在方法表的屬性集合之中, 但並非所有的方法表都必須存在這個屬性, 譬如介面或者抽象類中的方法就不存在Code屬性。
Code屬性是Class檔案中最重要的一個屬性, 如果把一個Java程式中的資訊分為程式碼(Code, 方法體裡面的Java程式碼) 和後設資料(Metadata, 包括類、 欄位、 方法定義及其他資訊) 兩部分, 那麼在整個Class檔案裡, Code屬性用於描述程式碼, 所有的其他資料專案都用於描述後設資料。
2、Exceptions屬性
Exceptions屬性的作用是列舉出方法中可能丟擲的受查異常(Checked Excepitons) , 也就是方法描述時在throws關鍵字後面列舉的異常。
3、LineNumberTable屬性
LineNumberTable屬性用於描述Java原始碼行號與位元組碼行號(位元組碼的偏移量) 之間的對應關係。並不是執行時必需的屬性, 但預設會生成到Class檔案之中, 可以在Javac中使用-g: none或-g: lines選項來取消或要求生成這項資訊。
4、LocalVariableTable及LocalVariableTypeTable屬性
LocalVariableTable屬性用於描述棧幀中區域性變數表的變數與Java原始碼中定義的變數之間的關係, 它也不是執行時必需的屬性, 但預設會生成到Class檔案之中, 可以在Javac中使用-g: none或-g: vars選項來取消或要求生成這項資訊
5、SourceFile及SourceDebugExtension屬性
SourceFile屬性用於記錄生成這個Class檔案的原始碼檔名稱。 這個屬性也是可選的, 可以使用Javac的-g: none或-g: source選項來關閉或要求生成這項資訊。 在Java中, 對於大多數的類來說, 類名和檔名是一致的, 但是有一些特殊情況(如內部類) 例外
SourceDebugExtension屬性用於儲存額外的程式碼除錯資訊。 典型的場景是在進行JSP檔案除錯時, 無法通過Java堆疊來定位到JSP檔案的行號。
6、ConstantValue屬性
ConstantValue屬性的作用是通知虛擬機器自動為靜態變數賦值。 只有被static關鍵字修飾的變數(類變數) 才可以使用這項屬性。 類似“int x=123”和“static int x=123”這樣的變數定義在Java程式裡面是非常常見的事情, 但虛擬機器對這兩種變數賦值的方式和時刻都有所不同。 對非static型別的變數(也就是例項變數) 的賦值是在例項構造器<init>()方法中進行的; 而對於類變數, 則有兩種方式可以選擇: 在類構造器<clinit>()方法中或者使用ConstantValue屬性。
7、InnerClasses屬性
InnerClasses屬性用於記錄內部類與宿主類之間的關聯。 如果一個類中定義了內部類, 那編譯器將會為它以及它所包含的內部類生成InnerClasses屬性
8、Deprecated及Synthetic屬性
Deprecated和Synthetic兩個屬性都屬於標誌型別的布林屬性, 只存在有和沒有的區別, 沒有屬性值的概念。
Deprecated屬性用於表示某個類、 欄位或者方法, 已經被程式作者定為不再推薦使用, 它可以通過程式碼中使用“@deprecated”註解進行設定
Synthetic屬性代表此欄位或者方法並不是由Java原始碼直接產生的, 而是由編譯器自行新增的, 在JDK 5之後, 標識一個類、 欄位或者方法是編譯器自動產生的, 也可以設定它們訪問標誌中的ACC_SYNTHETIC標誌位。
9、StackMapTable屬性
StackMapTable是一個相當複雜的變長屬性, 位於Code屬性的屬性表中。 這個屬性會在虛擬機器類載入的位元組碼驗證階段被新型別檢查驗證器(TypeChecker), 目的在於代替以前比較消耗效能的基於資料流分析的型別推導驗證器。
StackMapTable屬性中包含零至多個棧對映幀(Stack Map Frame) , 每個棧對映幀都顯式或隱式地代表了一個位元組碼偏移量, 用於表示執行到該位元組碼時區域性變數表和運算元棧的驗證型別。 型別檢查驗證器會通過檢查目標方法的區域性變數和運算元棧所需要的型別來確定一段位元組碼指令是否符合邏輯約束。
10、Signature屬性
Signature屬性是一個可選的定長屬性, 可以出現於類、 欄位表和方法表結構的屬性表中。 任何類、 介面、 初始化方法或成員的泛型簽名如果包含了型別變數(Type Variable) 或引數化型別(ParameterizedType) , 則Signature屬性會為它記錄泛型簽名資訊。 之所以要專門使用這樣一個屬性去記錄泛型型別, 是因為Java語言的泛型採用的是擦除法實現的偽泛型, 位元組碼(Code屬性) 中所有的泛型資訊編譯(型別變數、 引數化型別) 在編譯之後都通通被擦除掉。
11、BootstrapMethods屬性
BootstrapMethods是一個複雜的變長屬性, 位於類檔案的屬性表中。 這個屬性用於儲存invokedynamic指令引用的引導方法限定符。
12、MethodParameters屬性
MethodParameters是一個用在方法表中的變長屬性。MethodParameters的作用是記錄方法的各個形參名稱和資訊。
13、模組化相關屬性
JDK 9的一個重量級功能是Java的模組化功能, 因為模組描述檔案(module-info.java) 最終是要編譯成一個獨立的Class檔案來儲存的, 所以, Class檔案格式也擴充套件了Module、 ModulePackages和ModuleMainClass三個屬性用於支援Java模組化相關功能。
Module屬性是一個非常複雜的變長屬性, 除了表示該模組的名稱、 版本、 標誌資訊以外, 還儲存了這個模組requires、 exports、 opens、 uses和provides定義的全部內容,
ModulePackages是另一個用於支援Java模組化的變長屬性, 它用於描述該模組中所有的包, 不論是不是被export或者open的。
ModuleMainClass屬性是一個定長屬性, 用於確定該模組的主類(Main Class)
參考:
《深入理解java虛擬機器第三版》