JVM學習筆記-第六章-類檔案結構
6.3 Class類檔案的結構
本章中,筆者只是通俗地將任意一個有效的類或介面鎖應當滿足的格式稱為“Class檔案格式”,實際上它完全不需要以磁碟的形式存在。
Class檔案是一組以8個位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎全都是程式執行的必要資料。當遇到需要佔用8個位元組以上空間的資料項時,則會按照高位在前的方式分割成若干個8個位元組進行儲存。Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料,這種偽結構中只有兩種資料型別:
- 無符號數,屬於基本的資料型別。以u1、u2、u4、u8來分別表示一個位元組、兩個位元組、四個位元組和八個位元組的無符號數。可以用來描述數字、索引引用、數量值、按照UTF-8編碼構成的字串。
- 表,由多個無符號數或者其他表作為資料項構成的複合資料型別,用於描述有層次關係的複合結構的資料。
無論時無符號數還是表,當需要描述同一型別但數量不定的多個數據時,經常會使用一個強制的容量計數器和若干個連續的資料項的形式,這時候稱這一系列連續的某一型別的資料為某一型別的“集合”。
6.3.1 魔數與Class檔案的版本
每個Class檔案的頭4個位元組被稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。使用魔數而不是副檔名來進行識別主要是基於安全考慮,因為副檔名可以隨意改動。
緊接著魔數的4個位元組儲存的是Class檔案的版本號:第五個和第六個位元組是次版本號(Minor Version),第七個和第八個位元組是主版本號(Major Version)。Java的版本號是從45開始的(對應主版本號)。若主版本號的值為0x0032,也就是十進位制的50,該版本號說明該Class檔案可以被JDK 6或以上版本的虛擬機器執行。
$$
可執行虛擬機器的最低版本=主版本號-45+1
$$
JDK 12之前次版本號均未被使用,全部固定為0。直到JDK 12時期,將它用於標識“技術預覽版”功能特性的支援。如果Class檔案中使用了該版本JDK尚未列入正式特性清單中的預覽功能,則必須把次版本號標識為65535,以便JAVA虛擬機器在載入類檔案時能夠區分出來。
6.3.2 常量池
常量池可以比喻為Class檔案裡的資源倉庫,它是Class檔案結構中與其他專案關聯最多的資料,通常也是佔用Class檔案空間最大的資料專案之一,還是在Class檔案中第一個出現的表型別資料專案。
由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2型別的資料,代表常量池容器計數值(constant_pool_count)。Class檔案結構中只有常量池的容量技術是從1開始,0用來表達“不引用任何一個常量池專案”。
常量池中主要存放兩大類常量:
- 字面量(Literal),比較接近於JAVA語言層面的常量概念。如:文字字串、被宣告為final的常量值等。
- 符號引用(Symbolic References),屬於編譯原理方面的概念。主要包括:
- 被模組匯出或者開放的包(Package)
- 類和介面的全限定名(Fully Qualified Name)
- 欄位的名稱和描述符(Descriptor)
- 方法的名稱和描述符
- 方法控制代碼和方法型別(Method Handle)
- 動態呼叫點和動態常量(Dynamically-Computed Call Site)
tips: Oracle公司提供了一個專門用於分析Class檔案位元組碼的工具:javap
後續的常量池中的17種資料型別的結構總表,如果需要檢視,沒有書的同學可以自行百度。
**6.3.3 訪問標誌 **
在常量池結束之後,緊接著的2個位元組代表訪問標誌(access_flags),用於識別一些類或者介面層次的訪問資訊。access_flags中一共有16個標誌位可以使用,到JDK 9時只定義了其中9個。沒有使用到的標誌位要求一律為0。
6.3.4 類索引、父類索引與介面索引集合
類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引集合(interfaces)是一組u2型別的資料的集合。Class檔案中由這三項資料來確定該型別的繼承關係。類索引用於確定這個類的全限定名,父類索引用於去欸的那個這個類的父類的全限定名(只有一個父類索引,除了Object類所有類的父類索引都不為0),介面索引集合就是用來描述這個類實現了哪些介面(這些被實現的介面將按implements關鍵字後的介面順序從左到右排列在介面索引集合中)。
類索引、父類索引和介面索引集合都按順序排列在訪問標誌之後,類索引和父類索引用兩個u2型別的索引值表示,它們各自指向一個型別為CONSTANT_Class_info的類描述符常量,通過CONSTANT_Class_info型別的常量中的索引值可以找到定義在CONSTANT_Utf8_info型別的常量中的全限定名字串。
對於介面索引集合,入口的第一項u2型別的資料為介面計數器(interfaces_count),表示索引表的容量。
6.3.5 欄位表集合
欄位表(field_info)用於描述介面或者類中宣告的變數。欄位修飾符放在access_flags專案中,是一個u2的資料型別。access_flags訪問標誌中兩項索引值:name_index 和 descriptior_index 都是對常量池的引用,分別代表著欄位的簡單名稱以及欄位和方法的描述符。
tips: 全限定名:把類全名中的"."替換成了"/"而已。最後會加入一個";"表示結束
tips:簡單名稱:沒有型別和引數修飾的方法或者欄位名稱
描述符的作用是用來描述欄位的資料型別、方法的引數列表和返回值。用描述符來描述方法時,按照先引數列表、後返回值的順序描述,引數列表按照引數的嚴格順序放在一組"( )"中。
欄位表所包含的固定資料專案到descriptor_index就全部結束了,不過在之後跟隨著一個屬性表集合,用於儲存一些額外的資訊。欄位表集合不會列出從父類或者父介面中繼承而來的欄位 。
6.3.6 方法表集合
方法表的結構如同欄位表一樣,依次包括訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)。僅在訪問標誌和屬性表集合的可選項中有所區別。因為有些欄位修飾符不能用於修飾方法,並且有些用於修飾方法的修飾符同樣也不能用於修飾欄位。
方法裡的JAVA程式碼,通過javac編譯器編譯成位元組碼指令之後,存放在方法屬性表集合中一個名為"Code"的屬性裡面。
6.3.7 屬性表集合
對於每一個屬性,它的名稱都要從常量池中引用一個CONSTANT_Utf8_info型別的常量來表示,而屬性值的結構則是完全自定義的,只需要通過一個u4的長度屬性去說明屬性值所佔用的位數即可。
1. Code屬性
JAVA程式方法體裡面的程式碼通過javac編譯器處理之後,最終變為位元組碼指令儲存在Code屬性內,但是介面或者抽象類中的方法不會存在於Code屬性內。Code屬性結構及作用:
型別 | 名稱 | 數量 | 作用 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性的屬性名稱 |
u4 | attribute_length | 1 | 屬性值的長度 |
u2 | max_stack | 1 | 運算元棧深度最大值 |
u2 | max_locals | 1 | 區域性變數所需儲存空間 |
u4 | code_length | 1 | 位元組碼長度 |
u1 | code | code_length | 一系列位元組流 |
u2 | exception_table_length | 1 | |
exception_info | exception_table | exception_table_length | |
u2 | attributes_count | 1 | |
attribute_info | attributes | attributes_count |
補充:
max_locals單位是變數槽(Slots),對於長度不超過32位的資料型別(byte,char,float,int,short,boolean,returnAddress 等),每個區域性變數佔用一個變數槽。對於double和long這兩種長度為64位的資料型別則需要兩個變數槽來存放。對於max_locals長度計算,由於Java虛擬機器中將區域性變數槽進行重用,當代碼執行超出一個區域性變數的作用域時,這個區域性變數所佔的變數槽可以被其他區域性變數所使用,所以將同時生存的最大區域性變數數量和型別計算出的結果作為max_locals的大小。
code_length雖然它是一個u4型別的長度值,但實際只是用了u2長度,超過這個限制就會拒絕編譯。因為在《Java虛擬機器規範》中限制了一個方法不允許超過65535條位元組碼指令。
2. Exceptions屬性
Exceptions屬性的作用時列舉出方法中可能丟擲的受查異常(Checked Exceptions),也就是方法描述時在throws關鍵字後面的列舉的異常。此屬性中的number_of_exceptions項表示方法可能丟擲number_of_exceptions種受查異常,每一種受查異常使用一個exception_index_table項表示。exception_index_table是一個指向常量池中CONSTANT_Class_info型常量的索引,代表了該受查異常的型別。
3. LineNumberTable屬性
LineNumberTable屬性用於描述Java原始碼行號與位元組碼行號(位元組碼的偏移量)之間的對應關係。它並不是必要屬性,但預設會生成到Class檔案中。如果選擇不生成,最主要的影響就是當丟擲異常時,堆疊中將不顯示出錯的行號,並且在除錯程式的時候,也無法按照原始碼行來設定斷點。
其中,line_number_info表包含start_pc和line_number兩個u2型別的資料項,分別表示位元組碼行號和Java原始碼行號。
4. LocalVariableTable及LocalVariableTypeTable屬性
LocalVariableTable屬性用於描述區域性變量表與Java原始碼中定義的變數之間的關係。不是執行時必需的屬性,但是預設會生成到Class檔案中。如果沒有生成這項屬性,最大的影響就是當其他人引用這個方法時,所有的引數名稱都將會丟失。
JDK 5引入泛型之後,LocalVariableTable屬性增加了一個類似的屬性-LocalVariableTypeTable,區別在於其使用了欄位的特徵簽名來完成泛型的描述。
5. SourceFile及SourceDebugExtension屬性
SourceFile屬性用於記錄生成這個Class檔案的原始碼檔名稱。這個屬性也是可選的。如果不生成的話,當丟擲異常時,堆疊中將不會顯示出錯程式碼所屬的檔名。
SourceDebugExtension屬性可以用於儲存這個標準所新加入的除錯資訊,譬如讓程式猿能夠快速從異常堆中定位出原始JSP中出現問題的行號。其結構中debug_extension儲存的就是額外的除錯資訊,是一組通過變長UTF-8格式來表示的字串。一個類中最多隻允許一個SourceDebugExtension屬性。
6. ConstantValue屬性
ConstantValue屬性的作用是通知虛擬機器自動為靜態變數賦值。只有被static關鍵字修飾的變數才可以使用這項屬性。
對於非static型別的變數的賦值是在例項構造器( )方法中進行的。對於這類變數有兩種方式可以選擇:
- 在類構造器( )方法中
- 使用ConstantValue屬性
目前按照Oracle公司實現的javac編譯器的選擇是:如果同時使用final和static來修飾一個變數,並且這個變數的資料型別是基本型別或者為java.lang.String的話,就將會生成ConstantValue屬性來進行初始化;如果這個變數沒有被final修飾,或者並非基本型別及字串,則將會選擇在( )方法中進行初始化。
《Java虛擬機器規範》中只要求ConstantValue屬性的欄位必須設定ACC_STATIC標誌而已,對final關鍵字的要求是javac編譯器自己加入的限制。
從資料結構中可以看出ConstantValue屬性是一個定長屬性,它的attribute_length資料值必須固定為2。
7. InnerClasses屬性
InnerClasses屬性用於記錄內部類和宿主類之間的關聯。如果一個類中定義了內部類,那編譯器將會為它以及它所包含的內部類生成InnerClasses屬性。
結構名稱 | 作用 |
---|---|
number_of_classes | 代表需要記錄多少內部類資訊 |
inner_classes_info | 描述每一個內部類的資訊 |
inner_class_info_index | 代表內部類符號引用 |
outer_class_info_index | 代表宿主類符號引用 |
inner_name_index | 代表整個內部類的名稱,如果是匿名內部類,這項值為0 |
inner_class_flags | 內部類的訪問標誌 |
8. Deprecated及Synthetic屬性
Deprecated及Synthetic兩個屬性都屬於標誌型別的布林屬性,只存在有和沒有的區別,沒有屬性值的概念。
Deprecated 屬性用於表示某個類、欄位或者方法,已經被程式作者定為不再推薦使用,可以通過程式碼中的"@deprecated"註解進行設定。
Synthetic 屬性代表此欄位或者方法並不是由Java原始碼直接產生的,而是由編譯器自行新增的。
它們兩個的結構相同,其中attribute_length資料項的值必須為0x00000000,因為沒有任何屬性值需要設定。
9. StackMapTable屬性
這個屬性會在虛擬機器類載入的位元組碼驗證階段被新型別檢查驗證器(Type Checker)使用,目的在於代替以前比較消耗效能的基於資料流分析的型別推導驗證器。
此新型別驗證器在同樣能保證Class檔案很發行的前提下,省略了在執行期間通過資料流分析去確認位元組碼的行為邏輯合法性的步驟,而在編譯階段將一系列的驗證型別(Verification Type)直接記錄在Class檔案中,通過檢查這些驗證型別代替了型別推導過程,從而大幅度提升了位元組碼驗證的效能。
StackMapTable屬性中包含零至多個棧對映幀(Stack Map Frame),每個棧對映幀都顯式或隱式地代表了一個位元組碼偏移量,用於表示執行到該位元組碼時區域性變量表和運算元棧的驗證型別。型別檢查驗證器會通過檢查目標方法的區域性變數和運算元棧所需要的型別來確定一段位元組碼指令是否符合邏輯約束。
10. Signature屬性
它是一個可選的定長屬性,可以出現於類、欄位表和方法表結構的屬性表中。如果Java中任何類、介面、初始化方法或成員的泛型簽名包含了型別變數或引數化型別,則此屬性會為它記錄泛型簽名信息。
現在Java的反射API能夠獲取的泛型型別,最終的資料來源也是這個屬性。
當Signature屬性是類檔案的屬性,則這個結構表示類簽名;如果當前的Signature屬性是方法表屬性,則這個結構表示方法型別簽名;如果當前Signature屬性是欄位表的屬性,則這個結構表示欄位型別簽名。
11. BootstrapMethods屬性
它是一個複雜的變長屬性,位於類檔案的屬性表中。這個屬性用於儲存invokedynamic指令引用的引導方法限定符。屬性中,num_bootstrap_methods項的值給出了bootstrap_methods[]陣列中的引導方法限定符的數量。陣列的每個成員代表了一個引導方法,還包含了這個引導方法靜態引數的序列(可能為空)。
12. MethodParameters屬性
JDK 8時新加入到Class檔案格式中,它是一個用在方法表中的變長屬性。作用是記錄方法的各個形參名稱和資訊。
這個屬性使得編譯器可以將方法名稱也寫進Class檔案中,而且它是方法表的屬性,可以執行時可以通過反射API獲取。其結構中:
name_index代表了該引數的名稱,access_flags是引數的狀態指示器。
13. 模組化相關屬性
Module屬性是一個非常複雜的變長屬性,除了表示該模組的名稱、版本、標誌資訊以外,還儲存了這個模組requires、exports、opens、uses和provides定義的全部內容。其結構中:
module_name_index 代表了該模組的名稱,module_flags是模組的狀態指示器,exports屬性的每一個元素都代表一個被模組所匯出的包。exports中:exports_index 代表了被該模組匯出的包,exports_flags是該匯出包的狀態指示器,exports_to_count是該匯出包的限定計數器(如果這個計數器為0,說明該匯出包是無限定的,任何其他模組都可以訪問該包中的所有內容),exports_to_index 是以計數器值為長度的陣列(每個元素代表著只有在這個陣列範圍內的模組才被允許訪問該匯出包內容)。
ModulePackages是另一個用於支援Java模組化的變長屬性,用於描述該模組中所有的包,不論是不是被export或者open的。其結構中:
package_count是package_index陣列的計數器,package_index中每個元素都代表了當前模組中的一個包。
ModuleMainClass屬性是一個定長屬性,用於確定該模組的主類。其結構中:main_class_index代表了該模組的主類。
14. 執行時註解相關屬性
RuntimeVisibleAnnotations是一個變長屬性,它記錄了類、欄位或方法的宣告上記錄執行時可見註解。我們使用反射API來獲取類、欄位或方法上的註解時,返回值就是通過這個屬性來取到的。其結構中:
num_annotations是annotations 陣列的計數器,annotations中每一個元素代表了一個執行時可見的註解。註解在Class檔案中以annotation結構來儲存。type_index該常量應以欄位描述符的形式表示一個註解。num_element_value_pairs是element_value_pairs陣列的計數器。element_value_pairs中每個元素都是一個鍵值對,代表該註解的引數和值。
6.4 位元組碼指令簡介
這部分的指令,需要用到的時候,在進行檢視翻閱即可,在這裡不做描述。