class文件和字節碼解析

通過《編譯原理》的相關學習,我們知道我們編寫的Java代碼最終會被翻譯成class文件。Class文件格式是JVM自己定義的用於表示Java類的二進制字節流規範,與操作系統本身無關,該文件格式正是Java代碼一次編譯,跨平臺運行的關鍵。

class文件


其中u 表示n個無符號字節,如u4 magic 表示magic的取值用4個無符號字節表示,cp_info描述常量池的結構,field_info描述字段的數據結構,method_info描述方法的數據結構,attribute_info描述屬性的數據結構。ClassFile結構各項的含義如下:

  1. magic: 魔數,用於標識當前Class文件的文件格式,JVM可據此判斷該文件是否可以被解析,目前固定爲0xCAFEBABE
  2. minor_version, major_version:minor_version是副版本號,major_version是主版本號,這兩個版本是生成Class文件時根據編譯的JDK版本來確定的,用標識編譯時的JDK版本,常見的一個異常Unsupported
    major.minor version 52.0就是因爲運行時的JDK版本低於編譯時的JDK版本,52是Java8的主版本號。
  3. constant_pool_count:常量池計數器,等於常量池中的成員數加1
  4. constant_pool:常量池,是一種表結構,包含class文件結構和子結構中引用的所有字符串常量,類或者接口名,字段名和其他常量,其有效索引範圍是1- (constant_pool_count-1)。其中類和接口名採用全限定形式,即在整個JVM中的絕對名稱,如java.lang.Object,方法名,字段名、局部變量名和形參名都採用非限定名,即在源代碼文件中使用相對名稱,如屬性名name。
  5. access_flags:用於表示某個類或者接口的訪問權限和屬性
  6. this_class:類索引,該值必須是對常量池中某個常量的一個有效索引值,該索引處的成員必須是一個CONSTANT_Class_info類型的結構體,表示這個class文件所定義的類和接口
  7. super_class:父類索引,同this_class,該值必須是對常量池中CONSTANT_Class_info類型常量的一個有效索引值,如果該值爲0,則只能表示java.lang.Object類,因爲該類是唯一一個沒有父類的類。
  8. interfaces_count:接口計數器,表示當前類或者接口的直接超接口的數量
  9. interfaces:接口表,是一個表結構,每個成員同this_class,必須是對常量池中CONSTANT_Class_info類型常量的一個有效索引值,其有效索引範圍爲0~interfaces_count,接口表中成員的順序與源代碼中給定的接口順序是一致的,interfaces[0]表示源代碼中最左邊的接口。
  10. fields_count:字段計數器,當前class文件所有字段的數量
  11. fields:字段表,是一個表結構,表中每個成員必須是filed_info數據結構,用於表示當前類或者接口的某個字段的完整描述,不包含從父類或者父接口繼承的字段
  12. methods_count:方法計數器,表示當前類方法表的成員個數
  13. methods:方法表,是一個表結構,表中每個成員必須是method_info數據結構,用於表示當前類或者接口的某個方法的完整描述,包含當前類或者接口定義的所有方法,如實例方法、類方法、實例初始化方法等,不包含從父類或者父接口繼承的方法
  14. attributes_count:屬性計數器,表示當前class文件attributes屬性表的成員個數
  15. attributes:屬性表,是一個表結構,表中每個成員必須是attribute_info數據結構,這裏的屬性是對class文件本身,方法或者字段的補充描述,如SourceFile屬性用於表示class文件的源代碼文件名。
// 爲是一個簡單的例子
public class ClassFileTest {
    private int a = 5;
    public int getA(){
        return this.a;
    }
    public static void main(String[] args) {
        new ClassFileTest().getA();
    }
}
// javap -v
public class ClassFileTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#25         // ClassFileTest.a:I
   #3 = Class              #26            // ClassFileTest
   #4 = Methodref          #3.#24         // ClassFileTest."<init>":()V
   #5 = Methodref          #3.#27         // ClassFileTest.getA:()I
   #6 = Class              #28            // java/lang/Object
   #7 = Utf8               a
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LClassFileTest;
  #16 = Utf8               getA
  #17 = Utf8               ()I
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               args
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               SourceFile
  #23 = Utf8               ClassFileTest.java
  #24 = NameAndType        #9:#10         // "<init>":()V
  #25 = NameAndType        #7:#8          // a:I
  #26 = Utf8               ClassFileTest
  #27 = NameAndType        #16:#17        // getA:()I
  #28 = Utf8               java/lang/Object
{
  public ClassFileTest();
    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_5
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 1: 0
        line 3: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LClassFileTest;

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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #3                  // class ClassFileTest
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: invokevirtual #5                  // Method getA:()I
        10: pop
        11: return
      LineNumberTable:
        line 10: 0
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  args   [Ljava/lang/String;
}

例如#2對應類型是Fieldref,表示爲ClassFileTest.a:I.
例如#5對應類型是Methodref,表示爲ClassFileTest.getA:()I.
什麼意思?

描述符

描述符有兩種,字段描述符和方法描述符,本質就是一個基於特定規則的字符串,其中字段描述符用來表示類,實例和局部變量的類型,具體如下:在這裏插入圖片描述
ClassFileTest.a:I 表示 ClassFileTest類中int成員變量a.
ClassFileTest.getA:()I.表示 ClassFileTest類中返回值是int的getA方法。

常量池

Java虛擬機指令不依賴類,接口,類實例或數組的運行時內存佈局,而是依賴依賴常量池表中的符號信息,常量池表中所有項都有如下通用格式:

cp_info{
   u1 tag;//類型標記,用於確定後面的info的格式,tag是一個字節
   u1 info[];//兩個或者多個字節,取決於tag的值
}

在這裏插入圖片描述
CONSTANT_Utf8_info:用於表示一個Utf8編碼的字符串

CONSTANT_Utf8_info{
   u1 tag;//tag取值1
   u2 length;//後面的byte數組的長度
   u1 bytes[length];//字符串對應的byte數組數據
}

CONSTANT_Class_info:用於表示一個Java類或者接口名

CONSTANT_Class_info{
   u1 tag;//tag取值7
   u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
}

CONSTANT_Fieldref_info:用於描述一個字段

CONSTANT_Fieldref_info{
   u1 tag;//tag取值9
   u2 class_index;//常量池中的有效索引,該索引處的成員必須是一個CONSTANT_Class_info結構,表示該字段所屬的類
   u2 name_and_type_index;//常量池中的有效索引,該索引處的成員必須是一個CONSTANT_NameAndType_info結構,該結構用於表示一個字段或者方法描述符。
}

CONSTANT_MethodType_info:用於記錄方法的類型信息,即方法描述符

CONSTANT_MethodType_info{
   u1 tag;//tag取值21
   u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
   u2 descriptor_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
}

更多類型請參考《Java虛擬機規範8版》

字段

feild_info{
   u2 access_flags;//字段標識
   u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
   u2 descriptor_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
   u2 attributes_count;//當前字段附加屬性數值
   attribute_info attributes[attributes_count];//屬性表中的每個成員
}

在這裏插入圖片描述

方法

method_info{
   u2 access_flags;//方法標識
   u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
   u2 descriptor_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
   u2 attributes_count;//當前字段附加屬性數值
   attribute_info attributes[attributes_count];//屬性表中的每個成員
}

在這裏插入圖片描述

屬性

ClassFile、filed_info、method_info結構和Code屬性都有屬性表,所有的屬性都通過attribute_info結構表示,其通用格式如下:

attribute_info{
   u2 attribute_name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
   u4 attribute_length;//表示後面的info信息的字節長度
   u1 info[attribute_length];//具體數據
}

Java8預定義了23種屬性(《Java虛擬機規範8版》中有介紹),例

Code:位於method_info的屬性表中,表示該方法的虛擬機指令及輔助信息,method_info中有且僅有一個Code屬性,其結構如下:

attribute_info{
   u2 attribute_name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
   u4 attribute_length;//表示後面的info信息的字節長度
   u2 max_stack;//當前方法操作數棧的最大深度
   u2 max_locals;//此方法引用局部變量表中的局部變量的個數,包含傳遞方法入參的局部變量
   u4 code_length;//後面的code數組的字節長度
   u1 code[code_length];//當前方法的虛擬機指令的數據
   u2 exception_table_length;//後面的exception_table數組的長度;
   {
      u2 start_pc;
      u2 end_pc;//try/catch的代碼範圍,具體來說是起止代碼對應的虛擬機指令在code數組中的索引
      u2 handler_pc;//異常處理邏輯的代碼的虛擬機指令在code數組中的索引
      u2 catch_type;//常量池中一個類型爲CONSTANT_Class_info的有效索引,表示捕獲的異常類型。
   }exception_table[exception_table_length];//此方法的捕獲的各異常的異常處理邏輯
}

用戶在編譯源代碼文件時可以添加新的屬性,只要JVM實現能夠正確識別該屬性即可,注意用戶自定義的屬性不能使用已有預定義屬性的屬性名

虛擬機指令集

C/C++的方法會被編譯成特定於CPU架構的彙編指令,然後交由CPU逐一執行,因爲彙編指令與CPU架構是強綁定的,所以C/C++程序在執行前需要在不同CPU架構的機器上編譯一遍。Java爲了實現一處編譯,跨平臺運行的目標,在彙編指令之上引入了一個獨立於平臺的中間層,虛擬機指令,由Java虛擬機規範提供指令標準定義,由Java虛擬機廠商提供指令實現,不同平臺的Java虛擬機都遵循相同的指令集規範,從而實現跨平臺運行目標。一個方法對應的一組虛擬機指令稱爲這個方法的字節碼(byte codes)。

具體虛擬機指令集請查詢《Java虛擬機規範8版》,例:

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LClassFileTest;
操作碼 助記符 指令含義 例子操作
42 aload_0 將第一個引用類型本地變量推送至棧頂 this對象放入棧頂
180 getfield 獲取指定類的實例字段,並將值壓入棧頂 獲取a的值
172 ireturn 從當前方法返回int 返回a的值

主要參考

《hotspot實戰》
《Java虛擬機規範8版》
Hotspot class文件和字節碼解析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章