1. 程式人生 > >類檔案結構(四)

類檔案結構(四)

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_info 00 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原始碼中定義的變數之間的關係。如果沒有這個表,在除錯期間無法獲得引數值。