類檔案結構(四)
Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料項嚴格按照順序緊湊地排列在Class檔案中,中間沒有任何分隔符,整個Class檔案儲存的內容幾乎全是程式執行的必要資料,沒有空隙存在。當遇到8位位元組以上空間的資料項時,則按照高位在前的方式分割成若干個8位位元組進行儲存。
根據Java虛擬機器規範,Class檔案格式採用一種類似c語言結構體的偽結構來儲存資料,這種偽結構中只有兩種資料結構:無符號數和表,後面的解析都以這兩種資料型別為基礎。
- 無符號數:屬於基本的資料型別,以u1、u2、u4、u8來分別代表一個位元組、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 | interface_count | 1 | 介面數量 |
u2 | interfaces | interface_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 | 屬性表 |
上面的表其實可以劃分為以下七個部分,這七個部分組成了一個完整的 Class 位元組碼檔案:
- 魔數與Class檔案版本
每個Class的頭4個位元組成為魔數,唯一作用就是確定class檔案時被虛擬機器接受的(0xCAFEBABE),緊接著是版本號第5、6位元組是次版本號,第7、8位元組是主版本號,我們寫一段程式編譯成class來解釋:
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
如圖是用16進位制編輯器檢視,開頭是0xCAFEBABE,次版本號0x0000,主版本號0x0034(即十進位制52),
- 常量池
緊接著主版本號之後的是常量池入口,它是class檔案中與其他專案關聯最多的資料型別,也是class檔案空間最大的資料專案之一,還是在class檔案中第一個出現的表型別資料專案。
緊接著主版本號的u2型別 0x0013 表示常量池常量的個數(constant_pool_count),那麼緊跟著就有 constant_pool_count - 1 個常量(從索引1開始,0號常量空出來代表不指向的時候考慮),使用javap -verbose TestClass.class
檢視常量池資訊:
拿第一個舉例說明:對應的十六進位制是0A 00 04 00 0F
第一個0A代表tag,代表著是一個方法表(CONSTANT_Methodref_info tag=10),00 04代表class_index(方法是哪個類的,4指向常量池的4號即Object),00 0F代表name_and_type_index,指向15號即方法名 方法簽名型別()V。下面給出tag和表對應的關係:
- 訪問標誌
在常量池結束之後,緊接著的兩個位元組代表訪問標記(access_flags),這個標誌用於識別一些類或者介面層次的訪問資訊,包括:這個Class是類還是介面、是否定義為public型別、是否定義為abstract型別等。具體的標誌位以及標誌的含義見下表
本例的位元組碼中這兩個位元組是 00 21,通過檢視我們並沒有發現有標誌值是 00 21 的標誌名稱。這是因為這裡的訪問標誌可能是由多個標誌名稱組成的,所以位元組碼檔案中的標誌值其實是多個值進行或運算的結果。
通過查閱上述表格,我們可以知道,00 21 由 00 01 和 00 20 進行或運算得來。也就是說該類的訪問標誌是 public 並且允許使用 invokespecial 位元組碼指令的新語義。 類索引、父類索引、介面索引
類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引集合是一組u2型別的資料的集合,Class檔案中由這三項資料來確定這個類的繼承關係。
類索引。類索引用於確定這個類的全限定名,它用一個 u2 型別的資料表示。這裡的類索引是 00 03 表示其指向了常量池中第 3 個常量,通過我們之前的分析,我們知道第 3 個常量其最終的資訊是 TestClass類。
父類索引。父類索引用於確定這個類的父類的全限定名,父類索引用一個u2型別的資料表示。這裡的父類索引是 00 04 表示其指向了常量池中第 4 個常量,通過我們之前的分析,我們知道第 4 個常量其最終的資訊是 Object 類。因為其並沒有繼承任何類,所以 TestClass類的父類就是預設的 Object 類。
介面索引。介面索引集合就用來描述哪個類實現了哪些介面,這些被實現的介面將按 implements 語句(如果這個類本身就是一個介面,則應當是extends語句)後的介面順序從左到右排列在介面索引集合中。對於介面索引集合,入口第一項是 u2 型別的資料為介面計數器(interfaces_count),表示索引表的容量,而在介面計數器後則緊跟著所有的介面資訊。如果該類沒有實現任何介面,則該計數器值為0,後面介面的索引表不再佔用任何位元組。
這裡 TestClass類的位元組碼檔案中,因為並沒有實現任何介面,所以緊跟著父類索引後的兩個位元組是0x0000,這表示該類沒有實現任何介面。因此後面的介面索引表為空。欄位表集合
欄位表集合用於描述介面或者類中宣告的變數。這裡說的欄位包括類級變數和例項級變數,但不包括在方法內部宣告的區域性變數。
在類介面集合後的2個位元組是一個欄位計數器,表示總有有幾個屬性欄位。在欄位計數器後,才是具體的屬性資料。欄位表的每個欄位用一個名為 field_info 的表來表示,field_info 表的資料結構如下所示:
本例中fileds_count對應0x0001又一個欄位,緊接著後面是field_info00 02 00 05 00 06 00 00
access_flags對應0x0002查詢對應表:
可知該欄位是private修飾,name_index對應0x0005查詢常量池欄位名m,descript_index對應0x0006查詢常量池索引6得到I,name這個I代表什麼呢?欄位型別?
可以看出對應的是int型別方法表集合
在欄位表後的 2 個位元組是一個方法計數器,表示類中總有有幾個方法。在欄位計數器後,才是具體的方法資料。方法表中的每個方法都用一個 method_info 表示,其資料結構如下:
methods_count對應:0x0002
代表有兩個method_info,有兩個方法我們就檢視第二個方法inc()
這是一個檢視位元組碼的視覺化工具,在文章開頭有連結,先看access_flag對應ox0001
,檢視方法訪問標識表:
可知該方法是public,name_inde對應0x000B
查詢常量池可知方法名inc
descriptor_index對應0x000C
可知方法簽名()I- 屬性表集合
屬性表在前面已經出現了很多次,類檔案、方法表、欄位表都可以攜帶自己的屬性表集合
Code屬性
Java程式方法體中的程式碼經過Javac編譯器處理後,最終變為位元組碼指令儲存在Code屬性內。Code屬性出現在方法表的屬性集合之中,但並非所有的方法表都必須存在這個屬性,譬如介面或者抽象類中的方法就不存在Code屬性。
1.attribute_name_index:指向常量池的索引,固定為Code
2.max_stack:運算元棧深度的最大值
3.max_locals:區域性變量表所需的儲存空間。max_locals的單位為Slot,小於32位的值(如int、byte、float)佔用1一個Slot,double、long佔用兩個Slot。並不是方法中定義了多少個區域性變數,就把這些區域性變數所佔Slot之和作為max_locals的值,當代碼執行超過一個區域性變數的作用域時,這個區域性變數佔用的Slot就可以被其他區域性變數所使用,Javac編譯器會根據變數的作用域來分配Slot給各個變數使用,然後計算max_locals的大小。
4.code_length:位元組碼長度
5.code:用於儲存位元組碼指令的一系列位元組流。每個指令都是一個u1型別資料,當虛擬機器讀取到code的一個位元組碼時,就可以找出這個位元組碼代表的是什麼指令,並且可以知道這條指令後面是否需要跟隨引數,以及引數應當如何理解。
6.exception_table:異常表如下。如果當位元組碼在第start_pc行到第end_pc行之間(不含end_pc行)出現了型別為catch_type或者其子類的異常(catch_type為指向一個CONSTANT_Class_info型常量的引用),則轉到第handler_pc行繼續處理。當catch_type的值為0時,代表任意異常情況都需要轉向到handler_pc行進行處理。
異常表實際上是Java程式碼的一部分,編譯器使用異常表而不是簡單的跳轉命令來實現Java異常及finally處理機制。
Exceptions屬性
Exceptions屬性:方法可能丟擲的異常
LineNumberTable
Java原始碼行號與位元組碼行號之間的對應關係,當丟擲異常時,行號就是從這裡獲取到的。
LocalVariableTable
描述棧幀中區域性變量表中的變數與Java原始碼中定義的變數之間的關係。如果沒有這個表,在除錯期間無法獲得引數值。