JVM系列:.class類檔案結構

2020-08-14 11:06:37

class檔案是以一組以8個位元組爲基礎單位的二進制流,各個數據專案嚴格按照順序緊湊地排列在檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎全部是程式執行的必要數據,沒有空隙存在。當遇到需要佔用8個位元組以上空間的數據項時,則會按照高位在前[插圖]的方式分割成若幹個8個位元組進行儲存。
Class檔案格式採用一種類似於C語言結構體的僞結構來儲存數據,這種僞結構中只有兩種數據型別:「無符號數」和「表」。

  • 無符號數:屬於基本的數據型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數位、索引參照、數量值或者按照UTF-8編碼構成字串值
  • 表:由多個無符號數或者其他表作爲數據項構成的複合數據型別,爲了便於區分,所有表的命名都習慣性地以「_info」結尾。
  • 在这里插入图片描述

範例demo

後續我們以以下程式碼對應的class檔案做分析,編譯採用JDK8

package test;

/**
 * @Author : Wugao
 * @Date : Created in 16:06 2020/7/1
 * @Modified By   :
 */
public class DemoTest {

    private static final String ABC = "1998";
    private int age;
    private String name;

    public static void main(String[] args) {
        System.out.println(ABC);
        DemoTest test = new DemoTest();
        test.age += 1;
    }

    public String getName(String name){
        return name;
    }

    public int getAge(){
        return age;
    }
}

魔數與Class檔案的版本

  1. 定義:每個Class檔案的頭4個位元組被稱爲魔數(Magic Number),它的唯一作用是確定這個檔案是否爲一個能被虛擬機器接受的Class檔案
  2. java的魔術:0xCAFEBABE
  3. java版本號從45開始
  4. 緊接着魔數的第5,6個位元組代表java次版本,第7,8個位元組代表java主版本,如下圖:0000代表次版本號爲0,0034代表主版本號爲52

java版本号.jpg

  1. 對應的java版本,上面的檔案代表是由jdk8編譯的

class文件版本号对应关系.jpg

常數池

緊接着主、次版本號之後的是常數池入口,常數池可以比喻爲Class檔案裡的資源倉庫,它是Class檔案結構中與其他專案關聯最多的數據,通常也是佔用Class檔案空間最大的數據專案之一,另外,它還是在Class檔案中第一個出現的表型別數據專案。

  • 常數池入口放置一個u2型別的數據,代表常數池容量計數值(constant_pool_count),容量計數從1開始,也就是值爲1時,容量爲0,值爲10時,容量爲9(偏移地址:0x00000008),如上面的class爲48,容量則爲47
  • 主要存放兩大類常數
    • 字面量:如文字字串、被宣告爲final的常數值
    • 符號參照:
      • 被模組導出或者開放的包(Package)
      • 類和介面的全限定名(Fully Qualified Name)
      • 欄位的名稱和描述符(Descriptor)
      • 方法的名稱和描述符
      • 方法控制代碼和方法型別(Method Handle、Method Type、Invoke Dynamic)
      • 動態呼叫點和動態常數(Dynamically-Computed Call Site、Dynamically-ComputedConstant)
  • 常數池入口之後是常數項
    • 常數項的第一位是u1的標誌位,代表接下來的常數的型別(共17種),如上面的class中爲7,對應就是類或這個介面的符號參照

常量池项目类型.jpg

  • CONSTANT_Class_info型常數的結構
    • tag爲標誌位
    • name_index是常數池的的索引值,此處爲0x0002,查詢這個索引位置的常數的標識爲01,查表得知爲CONSTANT_Utf8_info

CONSTANT_Class_info型常量的结构.jpg

  • CONSTANT_Utf8_info
    • length(u2): 字串長度,位元組數,最大值2^16 - 1,因此類名或方法名最大位元組長度不能大於這個值,否則編譯失敗
    • bytes:length個位元組的位元組碼

CONSTANT_Utf8_info.jpg

  • CONSTANT_String_info字串型別字面量
    • index(u2): 指向字串字面量的索引,

CONSTANT_String_info.jpg

  • 所有17種常數池型別結構總表如下,需要時可以直接查表。另外可以使用javap -verbose Test.class檢視類結構

常量池17种类型数据结构总表1.jpg常量池17种类型数据结构总表2.jpg常量池17种类型数据结构总表3.jpg

存取標誌

  1. 緊接着常數池的是存取標識,u2,這個標誌用於識別一些類或者介面層次的存取資訊,包括:這個Class是類還是介面;是否定義爲public型別;是否定義爲abstract型別;如果是類的話,是否被宣告爲final;等等。

访问标识.jpg

  1. 本例中是public,因此ACC_PUBLIC,ACC_SUPER爲真,一剎那0x0001|0x0020=0x0021

類索引、父類別索引與介面索引集合

類索引(this_class)和父類別索引(super_class)都是一個u2型別的數據,而介面索引集合(interfaces)是一組u2型別的數據的集合
此處類索引和父類別索引分別是0001和0003,即常數池中索引爲1和3的型別
接下來的u2是介面的個數,此處爲0,如果不爲0,後續將跟上N個介面索引指向常數池中的型別

欄位表集合

  • 接下來的兩個位元組代表欄位的個數
  • 然後是欄位資訊,Java語言中的「欄位」(Field)包括類級變數以及範例級變數,但不包括在方法內部宣告的區域性變數,此處爲0003,包括ABC, age,name3個欄位

字段表结构.jpg

  • 欄位存取標識(access_flags)
    • ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED只能選擇其中一個
    • ACC_FINAL、ACC_VOLATILE不能同時選

字段访问标识.jpg

  • 簡單名稱索引(u2):指向常數池中的utf-8字串
  • 描述符索引(u2):對於欄位的描述符指向常數池中欄位的型別
  • 對於欄位或者方法,可以帶有屬性,接下來跟着的是屬性的個數(u2),後面根據屬性的不同,屬性的介面也不同,如此處的ABC,會帶有一個ConstantValue屬性,結構爲屬性名索引,屬性長度固定2,屬性值索引

方法表集合

  • 跟欄位表集合類似,首先是方法的個數,然後是方法的資訊
  • 方法結構表包括兩個部分,方法基本資訊,屬性

方法表结构.jpg

  • access_flags: 方法存取標識

方法访问标识.jpg

  • name_index: 簡單方法名的常數池索引
  • descriptor_index: 描述符的常數池索引,這裏描述了方法的參數,返回值,比如有一個方法是java.lang.Boolean check(String abc),那麼此處對應的常數應該是(Ljava/lang/String;)Z

規則是【左括號 + 參數型別 + 右括號 + 返回值型別】這裏爲什麼是Z呢?遵循以下規則
描述符标识字符含义.jpg
构造器方法.jpg

  • 舉例:該類有4個方法,爲什麼有四個方法呢?我們程式碼中只有main方法,getName,getAge方法,是因爲每個函數在沒有申明有參構造器時都有一個隱藏的預設構造方法
    • 0001(1)代表該方法的方法存取識別符號:查表得知是ACC_PUBLIC

    • 000d(13)代表方法的簡單名稱的常數索引,查詢得知是

    • 000e(14)代表存取描述符常數池索引,查詢得知是()V,表示無參,無返回

    • 0001(1)代表有一個屬性

    • 000f(15)代表屬性名稱索引,查詢得知是Code,Code屬性表結構如下 Code属性表结构.jpg

      • 0000002f(37):代表屬性長度

      • 0001(1):運算元棧最大深度,在執行這個方法時,運算元棧都不會超過這個深度

      • 0001(1):區域性變數表所需的儲存空間,這裏只變量槽的數量

      • 0000 0005(5):代表JVM指令長度位元組數,後續跟這個的5個位元組就是5個指令

      • 2a,b7,00,10,b1:分別代表5個指令,可通過查詢虛擬機器位元組碼指令表獲得

      • 0000(0):代表顯示申明的Exception的個數,這裏爲0,如果不爲0

        • 如果存在異常表,那它的格式應如所示,包含四個欄位,這些欄位的含義爲:如果當位元組碼從第start_pc行[插圖]到第end_pc行之間(不含第end_pc行)出現了型別爲catch_type或者其子類的異常(catch_type爲指向一個CONSTANT_Class_info型常數的索引),則轉到第handler_pc行繼續處理。當catch_type的值爲0時,代表任意異常情況都需要轉到handler_pc處進行處理 异常描述表结构.jpg
      • 0002(2):代表屬性的個數

      • 0012(18):代表屬性名稱對應常數池索引,這裏是LineNumberTable

        • 0000 0006:代表屬性長度
        • 0001:代表屬性表長度,後面跟着N個line_number_info,這個line_number_info有兩個u2組成,第一個是位元組碼行號,第二個是java原始碼行號
        • 0000和0008分別代表位元組碼的0行對應java原始碼的第8行 LineNumberTable属性结构.jpg
      • 0013(19):代表屬性名稱對應常數池索引,這裏是LocalVariableTable

        • 0000 000c(12):代表屬性長度
        • 0001代表本地變數表的長度,跟着n個local_variable_info,這個表的結構有u2:start_pc,u2:length,u2:name_index,u2:descriptor_index,u2:index
        • 0000,0005,0014,0015,0000:分別表示區域性變數生命週期開始的位元組碼偏移量,作用範圍覆蓋的長度,名稱常數池索引,描述符常數池索引,佔用變數槽的位置 在这里插入图片描述
      • 至此,一個方法分析就完了,後面跟着的就是類中的其他方法,方法是所有型別中最複雜的,學會了這個,其他應該就簡單了。