如何閱讀JAVA 位元組碼(一)

徐家三少發表於2017-02-21

  在閱讀JAVA位元組碼以前,需要回憶一下JVM的結構:

如何閱讀JAVA 位元組碼(一)

  Java位元組碼的資訊主要在Java棧中間體現,下圖來自網路,描述了java棧的基本結構:
如何閱讀JAVA 位元組碼(一)

  值得注意的是方法區,在Java虛擬機器中,方法區(Method Area)是可供各條執行緒共享的執行時記憶體區域。方法 區與傳統語言中的編譯程式碼儲存區(Storage Area Of Compiled Code)或者作業系統程式的正文段(TextSegment)的作用非常類似,它儲存了每一個類的結構資訊,例如執行時常量池(RuntimeConstantPool)、欄位和方法資料、建構函式和普通方法的位元組碼內容、還包括一些在類、例項、介面初始化時用到的特殊方法。

  每一個方法從呼叫開始到執行完成的過程,就對應著一個棧幀在虛擬機器棧裡面從入棧到出棧的過程。

   對於執行引擎來說,活動執行緒中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法。執行引擎所執行的所有位元組碼指令都只針對當前棧幀進行操作。

  關於棧幀內部4個區域的含義,直接引用了JAVA虛擬機器規範當中的內容。

區域性變數表:

  每個棧幀內部都包含一組稱為區域性變數表(LocalVariables)的變數列表。棧幀中區域性變數表的長度由編譯期決定,並且儲存於類和介面的二進位制表示之中,既通過方法的Code屬性儲存及提供給棧幀使用。

  一個區域性變數可以儲存一個型別為 booleanbytecharshortfloatreferencereturnAddress的資料,兩個區域性變數可以儲存一個型別為 longdouble的資料。

  區域性變數使用索引來進行定位訪問,第一個區域性變數的索引值為零,區域性變數的索引值是從零 至小於區域性變數表最大容量的所有整數。

  longdouble型別的資料佔用兩個連續的區域性變數,這兩種型別的資料值採用兩個區域性變 量之中較小的索引值來定位。例如我們講一個 double 型別的值儲存在索引值為 n 的區域性變數中, 實際上的意思是索引值為 nn+1 的兩個區域性變數都用來儲存這個值。索引值為n+1的區域性變數是無法直接讀取的,但是可能會被寫入,不過如果進行了這種操作,就將會導致區域性變數n的內容失效掉。

  上文中提及的區域性變數 n 的 n 值並不要求一定是偶數,Java 虛擬機器也不要求 double 和 long 型別資料採用 64 位對其的方式存放在連續的區域性變數中。虛擬機器實現者可以自由地選擇適當的方 式,通過兩個區域性變數來儲存一個 double 或 long 型別的值。

  Java 虛擬機器使用區域性變數表來完成方法呼叫時的引數傳遞,當一個方法被呼叫的時候,它的 引數將會傳遞至從 0 開始的連續的區域性變數表位置上。特別地,當一個例項方法被呼叫的時候,第0個區域性變數一定是用來儲存被呼叫的例項方法所在的物件的引用(即 Java 語言中的“this” 關鍵字)。後續的其他引數將會傳遞至從 1開始的連續的區域性變數表位置上。

  說白了,區域性變數表就是儲存方法引數和區域性變數的地方。

運算元棧

  每一個棧幀內部都包含一個稱為運算元棧(Operand Stack)的後進先出 (Last-In-First-Out,LIFO)棧。棧幀中運算元棧的長度由編譯期決定,並且儲存於類和接 口的二進位制表示之中,既通過方法的 Code 屬性儲存及提供給棧幀使用。

在上下文明確,不會產生誤解的前提下,我們經常把“當前棧幀的運算元棧”直接簡稱為“操 作數棧”。
  運算元棧所屬的棧幀在剛剛被建立的時候,運算元棧是空的。

Java 虛擬機器提供一些位元組碼指 令來從區域性變數表或者物件例項的欄位中複製常量或變數值到運算元棧中,也提供了一些指令用於 從運算元棧取走資料、運算元據和把操作結果重新入棧。在方法呼叫的時候,運算元棧也用來準備 呼叫方法的引數以及接收方法返回結果

  舉個例子,iadd 位元組碼指令的作用是將兩個 int 型別的數值相加,它要求在執行的之前操作 數棧的棧頂已經存在兩個由前面其他指令放入的 int 型數值。在 iadd 指令執行時,2 個 int 值 從操作棧中出棧,相加求和,然後將求和結果重新入棧。在運算元棧中,一項運算常由多個子運算 (Subcomputations)巢狀進行,一個子運算過程的結果可以被其他外圍運算所使用。

  每一個運算元棧的成員(Entry)可以儲存一個 Java 虛擬機器中定義的任意資料型別的值,包 括 long 和 double 型別。
  在運算元棧中的資料必須被正確地操作,這裡正確操作是指對運算元棧的操作必須與運算元棧 棧頂的資料型別相匹配,例如不可以入棧兩個 int 型別的資料,然後當作 long 型別去操作他們, 或者入棧兩個 float 型別的資料,然後使用 iadd 指令去對它們進行求和。有一小部分 Java 虛 擬機指令(例如 dup 和 swap 指令)可以不關注運算元的具體資料型別,把所有在執行時資料區 中的資料當作裸型別(Raw Type)資料來操作,這些指令不可以用來修改資料,也不可以拆散那 些原本不可拆分的資料,這些操作的正確性將會通過 Class 檔案的校驗過程來強制保障。
  在任意時刻,運算元棧都會有一個確定的棧深度,一個 long 或者 double 型別的資料會佔用 兩個單位的棧深度,其他資料型別則會佔用一個單位深度。

動態連結

  每一個棧幀內部都包含一個指向執行時常量池的引用來支援當前方法 的程式碼實現動態連結(Dynamic Linking)。在 Class 檔案裡面,描述一個方法呼叫了其他方法, 或者訪問其成員變數是通過符號引用(Symbolic Reference)來表示的,動態連結的作用就是 將這些符號引用所表示的方法轉換為實際方法的直接引用。類載入的過程中將要解析掉尚未被解析 的符號引用,並且將變數訪問轉化為訪問這些變數的儲存結構所在的執行時記憶體位置的正確偏移 量。
  由於動態連結的存在,通過晚期繫結(Late Binding)使用的其他類的方法和變數在發生 變化時,將不會對呼叫它們的方法構成影響。

分析Class檔案的位元組碼

  假設有這樣一個JAVA檔案:

public class HelloWorld
{
    String str = "";

    public String getStr()
    {
        return str;
    }

    public void setStr(String str)
    {
        this.str = str;
    }

}複製程式碼

  編譯.Java檔案:
javac -g HelloWorld.java
  輸出位元組碼:
javap -verbose HelloWorld
  顯示如下的內容:

public class test01.HelloWorld
  SourceFile: "HelloWorld.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#21         //  java/lang/Object."<init>":()V
   #2 = String             #22            //
   #3 = Fieldref           #4.#23         //  test01/HelloWorld.str:Ljava/lang/String;
   #4 = Class              #24            //  test01/HelloWorld
   #5 = Class              #25            //  java/lang/Object
   #6 = Utf8               str
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Ltest01/HelloWorld;
  #15 = Utf8               getStr
  #16 = Utf8               ()Ljava/lang/String;
  #17 = Utf8               setStr
  #18 = Utf8               (Ljava/lang/String;)V
  #19 = Utf8               SourceFile
  #20 = Utf8               HelloWorld.java
  #21 = NameAndType        #8:#9          //  "<init>":()V
  #22 = Utf8
  #23 = NameAndType        #6:#7          //  str:Ljava/lang/String;
  #24 = Utf8               test01/HelloWorld
  #25 = Utf8               java/lang/Object
{
  java.lang.String str;
    flags:

  public test01.HelloWorld();
    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: ldc           #2                  // String
         7: putfield      #3                  // Field str:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 5: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      11     0  this   Ltest01/HelloWorld;

  public java.lang.String getStr();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field str:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Ltest01/HelloWorld;

  public void setStr(java.lang.String);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #3                  // Field str:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 16: 0
        line 17: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       6     0  this   Ltest01/HelloWorld;
               0       6     1   str   Ljava/lang/String;
}複製程式碼

  說明:
  javac -g TestClass.java

  • -g:生成所有的除錯資訊,包括區域性變數名和行號資訊。
    javap -c TestClass > TCC.txt,對於javap常用的引數:
  • -c:輸出位元組碼Code
  • -l(小寫L):輸出Code、LineNumberTable與LocalVariableTable
  • -s:輸出方法簽名(方法的接收引數列表和返回值)
  • -verbose:包含-c、-l以及輸出class檔案的編譯版本,常量池,Stack, Locals, Args_size
  • 對於javap而言,常用的就是-c或-verbose

  使用16進位制的Hex Fiend 檢視位元組檔案得到如下資訊:


CA FE BA BE 00 00 00 33 00 1A 0A 00 05 00 15 08 00 16 09 00 04 00 17 07 00 18 07 00 19 01 00 03 73 74 72 01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 13 4C 74 65 73 74 30 31 2F 48 65 6C 6C 6F 57 6F 72 6C 64 3B 01 00 06 67 65 74 53 74 72 01 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 06 73 65 74 53 74 72 01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0F 48 65 6C 6C 6F 57 6F 72 6C 64 2E 6A 61 76 61 0C 00 08 00 09 01 00 00 0C 00 06 00 07 01 00 11 74 65 73 74 30 31 2F 48 65 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 04 00 05 00 00 00 01 00 00 00 06 00 07 00 00 00 03 00 01 00 08 00 09 00 01 00 0A 00 00 00 39 00 02 00 01 00 00 00 0B 2A B7 00 01 2A 12 02 B5 00 03 B1 00 00 00 02 00 0B 00 00 00 0A 00 02 00 00 00 05 00 04 00 07 00 0C 00 00 00 0C 00 01 00 00 00 0B 00 0D 00 0E 00 00 00 01 00 0F 00 10 00 01 00 0A 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 03 B0 00 00 00 02 00 0B 00 00 00 06 00 01 00 00 00 0B 00 0C 00 00 00 0C 00 01 00 00 00 05 00 0D 00 0E 00 00 00 01 00 11 00 12 00 01 00 0A 00 00 00 3E 00 02 00 02 00 00 00 06 2A 2B B5 00 03 B1 00 00 00 02 00 0B 00 00 00 0A 00 02 00 00 00 10 00 05 00 11 00 0C 00 00 00 16 00 02 00 00 00 06 00 0D 00 0E 00 00 00 00 00 06 00 06 00 07 00 01 00 01 00 13 00 00 00 02 00 14複製程式碼

  先來看看Class的檔案結構:

ClassFile { 
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1]; 
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; field_info fields[fields_count]; 
    u2 methods_count; method_info methods[methods_count]; 
    u2 attributes_count; attribute_info attributes[attributes_count]; 
}複製程式碼

  其中u1、u2、u4分別代表1、2、4個位元組無符號數。

magic:

   魔數,魔數的唯一作用是確定這個檔案是否為一個能被虛擬機器所接受的Class檔案。魔數值固定為0xCAFEBABE,不會改變。

## minor_version、major_version:
  分別為Class檔案的副版本和主版本。它們共同構成了Class檔案的格式版本號。不同版本的虛擬機器實現支援的Class檔案版本號也相應不同,高版本號的虛擬機器可以支援低版本的Class檔案,反之則不成立。

constant_pool_count:

    常量池計數器,constant_pool_count的值等於constant_pool表中的成員數加1。

## constant_pool[]:
   常量池,constant_pool是一種表結構,它包含Class檔案結構及其子結構中引用的所有字串常量、類或介面名、欄位名和其它常量。常量池不同於其他,索引從1開始到constant_pool_count -1。

## access_flags:
   訪問標誌,access_flags是一種掩碼標誌,用於表示某個類或者介面的訪問許可權及基礎屬性。access_flags的取值範圍和相應含義見下表:

如何閱讀JAVA 位元組碼(一)

### this_class:
  類索引,this_class的值必須是對constant_pool表中專案的一個有效索引值。constant_pool表在這個索引處的項必須為CONSTANT_Class_info型別常量,表示這個Class檔案所定義的類或介面。

## super_class:
  父類索引,對於類來說,super_class的值必須為0或者是對constant_pool表中專案的一個有效索引值。如果它的值不為0,那constant_pool表在這個索引處的項必須為CONSTANT_Class_info型別常量,表示這個Class檔案所定義的類的直接父類。當然,如果某個類super_class的值是0,那麼它必定是java.lang.Object類,因為只有它是沒有父類的。

interfaces_count:

   介面計數器,interfaces_count的值表示當前類或介面的直接父介面數量。

interfaces[]:

  介面表,interfaces[]陣列中的每個成員的值必須是一個對constant_pool表中專案的一個有效索引值,它的長度為interfaces_count。每個成員interfaces[i] 必須為CONSTANT_Class_info型別常量。

## fields_count:
  欄位計數器,fields_count的值表示當前Class檔案fields[]陣列的成員個數。

## fields[]:
  欄位表,fields[]陣列中的每個成員都必須是一個fields_info結構的資料項,用於表示當前類或介面中某個欄位的完整描述。

## methods_count:
  方法計數器,methods_count的值表示當前Class檔案methods[]陣列的成員個數。

## methods[]:
  方法表,methods[]陣列中的每個成員都必須是一個method_info結構的資料項,用於表示當前類或介面中某個方法的完整描述。

## attributes_count:
  屬性計數器,attributes_count的值表示當前Class檔案attributes表的成員個數。

## attributes[]:
  屬性表,attributes表的每個項的值必須是attribute_info結構。
  重新看一下Class File的表結構:


ClassFile { 
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1]; 
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; field_info fields[fields_count]; 
    u2 methods_count; method_info methods[methods_count]; 
    u2 attributes_count; attribute_info attributes[attributes_count]; 
}複製程式碼

如何閱讀JAVA 位元組碼(一)

  可以看到前4個位元組為魔數,也就是0xCAFEBABE,這裡都是十六進位制。接下來兩個位元組是副版本號,是00。
如何閱讀JAVA 位元組碼(一)

  再接下來是副版本號:
如何閱讀JAVA 位元組碼(一)

  0x0033 轉換成十進位制是51。
  接下來是常量池個數。
如何閱讀JAVA 位元組碼(一)

  0x001A 轉換成10進位制是26。但按理說應該是從索引0到25,但實際上只有1到25。
  接下來的資料就是常量池的資料。
  所有常量池的資料都有如下的通用結構:

cp_info 
{ 
  u1 tag; 
  u1 info[]; 
}複製程式碼

  以1個位元組的tag開頭,後面info[]項的內容tag由的型別所決定。tag有效的型別和對應的取值如下表:
  

如何閱讀JAVA 位元組碼(一)

  下面我們來介紹下不同型別的tag所對應的結構和規則:
  CONSTANT_Class_info結構:
  CONSTANT_Class_info結構用於表示類或介面,格式如下:

CONSTANT_Class_info 
{ 
    u1 tag; 
    u2 name_index; 
}複製程式碼

  CONSTANT_Class_info結構的tag項的值為CONSTANT_Class(7)。name_index項的值,必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,代表一個有效的類或介面二進位制名稱的內部形式。
  CONSTANT_Fieldref_info, CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info結構:
  欄位,方法和介面方法由類似的結構表示:

CONSTANT_Fieldref_info 
{ 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}
CONSTANT_Methodref_info 
{ 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}
CONSTANT_InterfaceMethodref_info 
{ 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}複製程式碼

  CONSTANT_Fieldref_info結構的tag項的值為CONSTANT_Fieldref(9)
  CONSTANT_Methodref_info結構的tag項的值為CONSTANT_Methodref(10)。 CONSTANT_InterfaceMethodref_info結構的tag項的值為CONSTANT_InterfaceMethodref(11)
  class_index項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Class_info結構,表示一個類或介面,當前欄位或方法是這個類或介面的成員。
  name_and_type_index項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_NameAndType_info結構,它表示當前欄位或方法的名字和描述符。
  CONSTANT_String_info結構:
  CONSTANT_String_info用於表示java.lang.String型別的常量物件,格式如下:

CONSTANT_String_info 
{
    u1 tag; 
    u2 string_index; 
}複製程式碼

  CONSTANT_String_info結構的tag項的值為CONSTANT_String(8)。string_index項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示一組Unicode碼點序列,這組Unicode碼點序列最終會被初始化為一個String物件。

CONSTANT_Integer_info和CONSTANT_Float_info結構:

  CONSTANT_Intrger_info和CONSTANT_Float_info結構表示4位元組(int和float)的數值常量: 

CONSTANT_Integer_info 
{ 
    u1 tag; 
    u4 bytes; 
} 
CONSTANT_Float_info 
{ 
    u1 tag; 
    u4 bytes; 
}複製程式碼

  CONSTANT_Integer_info結構的bytes項表示int常量的值,按照Big-Endian的順序儲存。 CONSTANT_Float_info結構的bytes項按照IEEE 754單精度浮點格式。表示float常量的值,按照Big-Endian的順序儲存。
  CONSTANT_Long_info和CONSTANT_Double_info結構:
  CONSTANT_Long_info和CONSTANT_Double_info結構表示8位元組(long和double)的數值常量:

CONSTANT_Long_info 
{ 
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
} 
CONSTANT_Double_info 
{ 
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
}複製程式碼

  在Class檔案的常量池中,所有的8位元組的常量都佔兩個表成員(項)的空間。如果一個CONSTANT_Long_info或CONSTANT_Double_info結構的項在常量池中的索引為n,則常量池中下一個有效的項的索引為n+2,此時常量池中索引為n+1的項有效但必須被認為不可用。
  CONSTANT_Long_info結構的tag項的值是CONSTANT_Long(5)。 CONSTANT_Double_info結構的tag項的值是CONSTANT_Double(6)。
  CONSTANT_Long_info結構中的無符號的high_bytes和low_bytes項用於共同表示long型常量,構造形式為((long) high_bytes << 32) + low_bytes,high_bytes和low_bytes都按照Big-Endian順序儲存。 CONSTANT_Double_info結構中的high_bytes和low_bytes共同按照IEEE 754雙精度浮點格式表示double常量的值。high_bytes和low_bytes都按照Big-Endian順序儲存。
  CONSTANT_NameAndType_info結構:
  CONSTANT_NameAndType_info結構用於表示欄位或方法,但是和前面介紹的三個表示欄位方法的結構不同,CONSTANT_NameAndType_info結構沒有標識出它所屬的類或介面,格式如下:

CONSTANT_NameAndType_info 
{ 
    u1 tag; 
    u2 name_index; 
    u2 descriptor_index; 
}複製程式碼

  CONSTANT_NameAndType_info結構的tag項的值為CONSTANT_NameAndType(12)。

  name_index項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info結構,這個結構要麼表示特殊的方法名,要麼表示一個有效的欄位或方法的非限定名。
  descriptor_index項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info(§4.4.7)結構,這個結構表示一個有效的欄位描述符或方法描述符。
  CONSTANT_Utf8_info結構:
  CONSTANT_Utf8_info結構用於表示字串常量的值:

CONSTANT_Utf8_info 
{ 
    u1 tag; 
    u2 length; 
    u1 bytes[length]; 
}複製程式碼

  CONSTANT_Utf8_info結構的tag項的值為CONSTANT_Utf8(1)。length項的值指明瞭bytes[]陣列的長度,bytes[]是表示字串值的byte陣列。
  CONSTANT_MethodHandle_info結構:
  CONSTANT_MethodHandle_info結構用於表示方法控制程式碼,結構如下:

CONSTANT_MethodHandle_info 
{
    u1 tag; 
    u1 reference_kind; 
    u2 reference_index; 
}複製程式碼

  CONSTANT_MethodHandle_info結構的tag項的值為CONSTANT_MethodHandle(15)reference_kind項的值必須在1至9之間(包括1和9),它決定了方法控制程式碼的型別。
  reference_index項的值必須是對常量池的有效索引,索引項和reference_kind的對應關係如下:
  CONSTANT_MethodType_info結構:
 CONSTANT_MethodType_info結構用於表示方法型別:

CONSTANT_MethodType_info 
{ 
    u1 tag; 
    u2 descriptor_index; 
}複製程式碼

  CONSTANT_MethodType_info結構的tag項的值為CONSTANT_MethodType(16)。descriptor_index項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示方法的描述符。
  CONSTANT_InvokeDynamic_info結構:
  CONSTANT_InvokeDynamic_info用於表示invokedynamic指令所使用到的引導方法、引導方法使用到動態呼叫名稱、引數和請求返回型別以及可以選擇性的附加被稱為靜態引數的常量序列。

CONSTANT_InvokeDynamic_info 
{ 
    u1 tag; 
    u2 bootstrap_method_attr_index; 
    u2 name_and_type_index; 
}複製程式碼

  CONSTANT_InvokeDynamic_info結構的tag項的值為CONSTANT_InvokeDynamic(18)。bootstrap_method_attr_index項的值必須是對當前Class檔案中引導方法表的bootstrap_methods[]陣列的有效索引。ame_and_type_index項的值必須是對當前常量池的有效索引,常量池在該索引處的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符

  #1 = Methodref          #5.#21         //  java/lang/Object."<init>":()V複製程式碼

Methodref 在常量型別表中間是10。觀察位元組碼:

如何閱讀JAVA 位元組碼(一)

0A 剛好是10。那麼接下來就是methodref的結構:

CONSTANT_Methodref_info 
{ 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}複製程式碼

如何閱讀JAVA 位元組碼(一)

class_index 為0x0005,name_and_type_index為0015。正好對應“#5.#21”

接下來就是按照這種模式分析就可以了。

注意#25 = Utf8 java/lang/Object 這就說明索引25儲存的是java/lang/Object這個UTF-8字串。UTF-8的字串如下:
6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74

如何閱讀JAVA 位元組碼(一)

常量池完畢以後,就是JAVA類本身的資訊了(javap 出現的資訊也可以看出來)

類本身的資訊

ClassFile { 
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1]; 
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; field_info fields[fields_count]; 
    u2 methods_count; method_info methods[methods_count]; 
    u2 attributes_count; attribute_info attributes[attributes_count]; 
}複製程式碼

常量池後面兩個位元組是訪問標誌access_flags:00 21
其中ACC_PUBLIC的值為0x0001,ACC_SUPER的值為0x0020,這個欄位是複合的。

欄位

  每個欄位(Field)都由field_info結構所定義,在同一個Class檔案中,不會有兩個欄位同時具有相同的欄位名和描述符。
  field_info結構格式如下: 

field_info
{ 
    u2 access_flags; 
    u2 name_index; 
    u2 descriptor_index; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}複製程式碼

  access_flags項的值是用於定義欄位被訪問許可權和基礎屬性的掩碼標誌。取值範圍如下表:
  name_index項的值必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示一個有效的欄位的非全限定名。
  descriptor_index項的值必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示一個有效的欄位的描述符。
  attributes_count的項的值表示當前欄位的附加屬性的數量。
  attributes表的每一個成員的值必須是attribute結構,一個欄位可以有任意個關聯屬性。

方法

  所有方法(Method),包括例項初始化方法和類初始化方法在內,都由method_info結構所定義。在一個Class檔案中,不會有兩個方法同時具有相同的方法名和描述符。method_info結構格式如下:

method_info 
{ 
    u2 access_flags; 
    u2 name_index; 
    u2 descriptor_index; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}複製程式碼

  access_flags項的值是用於定義當前方法的訪問許可權和基本屬性的掩碼標誌,取值範圍如下表:
  
  標記名        值        說明

  ACC_PUBLIC     0x0001     public,方法可以從包外訪問

  ACC_PRIVATE    0x0002     private,方法只能本類中訪問

  ACC_PROTECTED  0x0004     protected,方法在自身和子類可以訪問

  ACC_STATIC     0x0008     static,靜態方法

  ACC_FINAL      0x0010     final,方法不能被重寫(覆蓋)

  ACC_SYNCHRONIZED     0x0020     synchronized,方法由管程同步

  ACC_BRIDGE     0x0040     bridge,方法由編譯器產生

  ACC_VARARGS     0x0080     表示方法帶有變長引數

  ACC_NATIVE     0x0100     native,方法引用非java語言的本地方法

  ACC_ABSTRACT   0x0400     abstract,方法沒有具體實現

  ACC_STRICT     0x0800     strictfp,方法使用FP-strict浮點格式

  ACC_SYNTHETIC   0x1000     方法在原始檔中不出現,由編譯器產生

  name_index項的值必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構。
  descriptor_index項的值必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示一個有效的方法的描述符。
  attributes_count的項的值表示這個方法的附加屬性的數量。attributes表的每一個成員的值必須是attribute結構,一個方法可以有任意個與之相關的屬性。

屬性:

  屬性(Attributes)在Class檔案格式中的ClassFile結構、field_info 結構,method_info結構和Code_attribute結構都有使用,所有屬性的通用格式如下:

attribute_info 
{ 
    u2 attribute_name_index; 
    u4 attribute_length; 
    u1 info[attribute_length]; 
}複製程式碼

  對於任意屬性,attribute_name_index必須是對當前Class檔案的常量池的有效16位無符號索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示當前屬性的名字。attribute_length項的值給出了跟隨其後的位元組的長度,這個長度不包括attribute_name_index和attribute_name_index項的6個位元組。
  對於欄位、方法、和屬性的結構,我們很容易的可以通過javap工具檢視到。
Class的檔案結構是十分複雜的,要在一篇部落格裡面講清楚是不太可能的,只能算是入門。有什麼問題的話可以在評論裡提問。

 

小廣告

新書《Java併發程式設計系統與模型》目前已經寫完一部分。但是實際上說實話,並不知道讀者們對java併發系統有哪些比較關心的,而且閉門造車實在是太累,寫出來怕沒人看。所以我放在這裡徵求大家的意見。大家可以直接在評論裡提問或者希望作者重點描寫哪一部分內容,我好有針對性的寫。

相關文章