1. 程式人生 > >Java位元組碼檔案深度剖析

Java位元組碼檔案深度剖析

Java位元組碼檔案檢視

我們有一個類Test01,具體內容如下:

package bytecode;

public class Test01 {
    private int i = 0;

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }
}

編譯這個類,得到Test01.class檔案

IDE檢視

用IDEA編譯器檢視

我們發現檢視到的class檔案與類檔案基本相同,這是因為IDE自帶的Fernflower decompiler將位元組碼檔案反編譯的結果。我們可以在外掛市場查詢安裝jclasslib外掛,來在IDEA中檢視class檔案。

hexedit檢視

通過hexedit直接檢視該位元組碼檔案

擴充套件:hexedit安裝

輸入:sudo apt install hexedit

當你啟動它時,你必須指定要開啟的檔案的位置,然後它會為你開啟它。

javap -verbose檢視

通過javap指令檢視位元組碼檔案:javap -verbose ****

執行javap -verbose指令,得到的結果如下:

Classfile /home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/bytecode/Test01.class
  Last modified 2019-12-3; size 460 bytes
  MD5 checksum 7913e827b66fbb2c05907b76dafa32ec
  Compiled from "Test01.java"
public class bytecode.Test01
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#21         // bytecode/Test01.i:I
   #3 = Class              #22            // bytecode/Test01
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               i
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lbytecode/Test01;
  #14 = Utf8               getI
  #15 = Utf8               ()I
  #16 = Utf8               setI
  #17 = Utf8               (I)V
  #18 = Utf8               SourceFile
  #19 = Utf8               Test01.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = NameAndType        #5:#6          // i:I
  #22 = Utf8               bytecode/Test01
  #23 = Utf8               java/lang/Object
{
  public bytecode.Test01();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field i:I
         9: return
      LineNumberTable:
        line 7: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lbytecode/Test01;

  public int getI();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field i:I
         4: ireturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lbytecode/Test01;

  public void setI(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field i:I
         5: return
      LineNumberTable:
        line 15: 0
        line 16: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lbytecode/Test01;
            0       6     1     i   I
}
SourceFile: "Test01.java"

使用javap -verbose命令分析一個位元組碼檔案時,將會分析該位元組碼檔案的魔數、版本號、常量池、類資訊、類的構造方法、類中的方法資訊、類變數與成員變數等資訊。

Java位元組碼檔案結構剖析

Class位元組碼中只有兩種資料型別:

  • 位元組資料直接量:屬於基本的資料型別,以u1、u2、u4、u8來分別代表1、2、4、8個位元組組成的整體資料。可以用來描述數字、索引引用、數量值、或者按照UTF-8編碼的字串值。
  • 表(陣列):由多個基本元素或者其他表,按照既定順序組成的大的資料集合。表是有結構的,它的結構體現在組成表的成分所在的位置和順序都是已經嚴格定義好的。

魔數(magic)

所有的.class位元組碼檔案的前4個位元組都是魔數,魔數值為固定值:0xCAFEBABE

版本號(version)

魔數之後的4個位元組是版本資訊,前2個位元組表示次版本號(minor version),後兩個位元組為主版本號(major version)。這裡的版本號為0x 00 00 00 34,表示次版本號為0,主版本號為52(這裡的52值的其實是jdk8,如果是jdk7的話就是51)。與指令返回的版本號一致。

  minor version: 0
  major version: 52

常量池(Constant pool)

緊接著主版本號之後的,就是常量池入口。一個Java類定義的很多資訊都是由常量池來維護和描述的,可以將常量池看作是class檔案的資源倉庫,比如說Java類中定義的方法與變數資訊,都是儲存在常量池中。

常量池中主要兩類常量:

  • 字面量:如文字字串,Java中宣告為final的常量值等
  • 符號引用:如類和介面的全侷限定名,欄位的名稱和描述符,方法的名稱和描述符等

常量池的總體結構:

Java類所對應的常量池主要由常量池數量與常量池表這兩部分組成:

  • 常量池數量緊跟在主版本號之後,佔據兩個位元組
  • 常量池表則緊跟在常量池數量之後

常量池表與一般的陣列不同的是,常量池表中不同的元素的型別、結構和長度都是不同的;但是,每一種元素的第一個資料都是一個u1型別,該位元組是個標識位,佔據一個位元組。JVM在解析產量池時,會根據這個u1型別來獲取元素的具體型別。

值得注意的是:常量池表中元素個數 = 常量池數量 - 1。其中0暫不使用,目的是滿足某些常量池索引值的資料在特定情況下表達【不引用任何常量池】的含義;根本原因在於索引0也是一個常量(保留常量),只不過它不位於常量表,這個常量就對應null值,所以常量池的索引是從1開始的。在本例中,0x 00 18代表常量池數量,常量池數量為24 - 1 = 23個,與javap的結果相同。

目前,常量池中出現的常量型別有14種,如下表:

後面三種都是Java7之後出現的,關於動態引用的。我們主要看前面11種,下表給出了前11個常量型別的詳細結構說明:

有了這兩張表就可以繼續剖析常量池的內容了。每一個常量的第一個位元組都是標誌位,第一個常量的標誌為0x 0A,轉換為十進位制為10,表示常量型別為:CONSTANT_Methodref_info,按照上表,第一個index為2個位元組,第二index也為2個位元組,0x 0A 00 04 00 14這五個位元組表示常量池中第一個常量,第一個index值為4,第二個index值為20,與javap反編譯的結果一致:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V

指向宣告欄位的類或者介面描述符CONSTANT_Class_info的索引項為4,4對應的是:
#4 = Class #23 // java/lang/Object

指向欄位描述符的CONSTANT_NameAndType_info的索引項為20,20對應的是:
#20 = NameAndType #7:#8 // "<init>":()V,這個又指向7和8:

  #7 = Utf8               <init>
  #8 = Utf8               ()V

PS:

  • 在JVM規範中,每個變數、欄位都有描述資訊,描述資訊主要的作用是描述欄位的資料型別、方法的引數列表(包括數量、型別與順序)與返回值。根據描述符規則,基本資料型別和的代表無返回值的void類用都用一個大寫字元表示,物件型別則使用大寫字元L加物件的全限定名稱來表示。為了壓縮位元組碼檔案的體積,JVM都只使用一個大寫字母表示,如下所示:
    • B - byte
    • C - char
    • D - double
    • F - float
    • I - int
    • J - Long
    • Z - boolean
    • V - void
    • L - 物件型別,如Ljava/lang/String;
  • 對於陣列型別來說,每一個維度使用一個前置的[來表示,如int[]被記錄為:[I,String[][]被記錄為:[[Ljava/lang/String;
  • 用描述符描述方法時,按照先引數列表、後返回值的順序來描敘。引數列表按照引數的嚴格順序放在一組()之內,如方法:String test( int id, String name)的描述符為:(I, Ljava/lang/String;)Ljava/lang/String;

位元組碼訪問標識(access_flags)

緊跟著常量池的是位元組碼訪問標識,佔據兩個位元組。訪問標誌資訊包括該Class檔案是類還是介面,是否被定義為public,是否是abstract,如果是類,是否被宣告成final。

本文例中0x 00 21是0x 0020與0x0001的並集,表示ACC_PUBLIC與ACC_SUPER。

類索引與父類索引

緊跟著位元組碼訪問標識的是類索引和父類索引,分別佔據兩個位元組。

本例中,0x 00 03表示類索引,在常量池中對應:#3 = Class #22 // bytecode/Test01
0x 00 04表示父類索引,在常量池中對應:#4 = Class #23 // java/lang/Object

介面(interfaces)

緊跟著父類索引的是介面,介面由兩部分組成:

  • 介面個數:介面個數佔據兩個位元組,本例中,介面個數為0x 00 00
  • 介面名:介面個數為0時,介面名不會出現,如果介面個數大於等於一,介面名才會出現,佔據兩個位元組

欄位(fields)

緊跟著介面的是欄位,欄位用於描述類和介面中宣告的變數。這裡的欄位包含了類級別變數以及例項變數,但是不包括方法內部宣告的區域性變數。

欄位由兩部分組成:

  • 欄位個數:欄位個數佔據兩個位元組,本文例中為0x 00 01,表示只有一個欄位

  • 欄位表:欄位表由4部分組成:

    • access_flags:訪問標識,佔據兩個位元組,本文例中為0x 00 02,表示為ACC_PRIVATE
    • name_index:名字,佔據兩個位元組,本文例中為0x 00 05,對應值為:** #5 = Utf8 i**
    • descriptor_index:描述符,佔據兩個位元組,本文例中為0x 00 06,對應值為:** #6 = Utf8 I**
    • attributes_count:屬性個數,佔據兩個位元組,本文例中為0x 00 00
型別 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

方法(method)

緊跟著欄位的是方法,方法由兩部分組成:

  • 方法個數:方法個數佔據2個位元組,本例中為0x 00 03,表示方法表中將由三個方法
  • 方法表:方法表的結構如下:

方法表

型別 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

方法中的每個屬性都是一個attribute_info結構,方法的屬性結構如下:

型別 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info[attribute_length] 1

JVM預定義了部分的attribute,但是編譯器自己也可以實現自己的attribute寫入class檔案裡,供執行時使用。不同的attribute通過attribute_name_index來區分。

Code結構

Code attribute的作用是儲存該方法的結構,如所對應的位元組碼:

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {
        u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
  • attribute_name_index:屬性名索引

  • attribute_length:表示attribute屬性所包含的位元組數,不包含attribute_name_index和attribute_length欄位

  • max_stack:表示這個方法執行的任何時刻所能達到的運算元棧的最大深度

  • max_locals:表示方法執行期間建立的區域性變數的數目,包含用來表達傳入的引數的區域性變數

  • code_length:表示該方法所包含的位元組碼的位元組數以及具體的指令碼

  • code[code_length]:具體的指令碼,是該方法被呼叫時,虛擬機器所執行的位元組碼

  • exception_table:存放的是處理異常的資訊,每個exception_table表項由start_pc、end_pc、handler_pc、catch_type組成

    • start_pc和end_pc表示在code陣列中的從start_pc和end_pc處(包含start_pc,不包含end_pc)的指令丟擲的異常會由這個表項來處理
    • handler_pc表示處理異常的程式碼的開始處
    • catch_type表示會被處理的異常型別,它指常量池中的一個異常類。當catch_type為0時,表示處理所有的異常
  • attributes_count:屬性值

    • 行號表(LineNumberTable):這個屬性用來表示code陣列的位元組碼和Java程式碼行數之間的關係,可以用來在除錯的時候定位程式碼執行的行數
    LineNumberTable_attribute {
      u2 attribute_name_index;
      u4 attribute_length;
      u2 line_number_table_length;
      {
          u2 start_pc;
          u2 line_number;
      }line_number_table[line_number_table_length];
    }
    • 區域性變量表(LocalVariableTable):
    LocalVariableTable_attribute {
      u2 attribute_name_index;
      u4 attribute_length; // 不包括起始6個位元組的屬性長度。
      u2 local_variable_table_length; // local_variable_table表中的項數。
      { 
          u2 start_pc;
          u2 length;
          u2 name_index;
          u2 descriptor_index;
          u2 index;
      } local_variable_table[local_variable_table_length];
    }

本文例位元組碼方法解讀

如圖所示,方法從0x 00 03,表示本位元組碼檔案由三個方法,這裡將會仔細解讀第一個方法:

  • 0x 00 01:access_flags,對應值為ACC_PUBLIC
  • 0x 00 07:name_index,對應常量池中的值為:
  • 0x 00 08:descriptor_index,對應常量池中的值為:()V
  • 0x 00 01:attributes_count,表示這個方法有一個屬性值
    • 0x 00 09:attribute_name_index,對應常量池中的值為:Code,表示這是一個Code attribute
    • 0x 00 00 00 38:attribute_length,屬性表長度,表示這個屬性的長度為56個位元組
    • 0x 00 02:max_stack
    • 0x 00 01:max_locals
    • 0x 00 00 00 0A:code_length,表示code的長度為10個位元組
    • 0x 2A B7 00 01 2A 03 B5 00 02 B1:code[code_length]
      • 0x 2A:aload_0
      • 0x B7 00 01:invokespecial #1
      • 0x 2A:aload_0
      • 0x 03:iconst_0
      • 0x B5 00 02:putfield #2
      • 0x B1:return
    • 0x 00 00:exception_table_length
    • 0x 00 02:attributes_count,表示該Code attribute有兩個屬性
      • 0x 00 0A:attribute_name_index,對應常量池中的值為:LineNumberTable
        • 0x 00 00 00 0A:attribute_length,表示這個行號表的長度為10
          • 0x 00 02:line_number_table_length,表示兩對對映
          • 0x 00 00 00 07:位元組碼偏移量為0,對映到原始碼的行號為7
          • 0x 00 04 00 08:位元組碼偏移量為4,對映到原始碼的行號為8
      • 0x 00 0B:attribute_name_index,對應常量池中的值為:LocalVariableTable
        • 0x 00 00 00 0C:attribute_length,表示這個行號表的長度為12
        • 0x 00 01:區域性變數的個數為1
        • 0x 00 00 00 0A:開始位置是0,結束位置是10
        • 0x 00 0C:區域性變數的值,對應常量池的值為:this
        • 0x 00 0D:區域性變數的描述,對應常量池的值為: Lbytecode/Test01;
        • 0x 00 00:索引值

至此,第一個方法解讀完畢,可以通過javap的結果對照檢視:

 public bytecode.Test01();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field i:I
         9: return
      LineNumberTable:
        line 7: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lbytecode/Test01;

屬性(Attributes)

0x 00 01 00 12 00 00 00 02 00 13

  • 0x 00 01:表示包含一個屬性
  • 0x 00 12:attribute_name_index,對應常量池中的值為:SourceFile
    • 0x 00 00 00 02:attribute_length,表示佔據兩個位元組
    • 0x 00 13:對應常量池中的值為:Test01.java