【JVM】JVM系列之Class檔案(三)

leesf發表於2016-03-12

一、前言

  隨著我們學習的不斷深入,我相信讀者對class檔案很感興趣,class檔案是使用者編寫程式與虛擬機器之前的橋樑,程式通過編譯形成class檔案,class檔案之後會載入虛擬機器,被虛擬機器執行,下面我麼來一起揭開class檔案的神祕面紗。

二、什麼是class檔案

  class檔案是二進位制檔案,通常是以.class檔案結尾的檔案,它是以8位位元組為基礎單位的二進位制流,各個資料項緊密排列在class檔案中,資料項的基本型別為u1,u2,u4,u8,分別表示一個位元組,兩個位元組,四個位元組,八個位元組的無符號數。

三、class檔案資料結構

  其實對於class檔案而言,總體的資料結構看上去很規整,具體的結構如下圖所示

  下面我們將用一個例子詳細講解class檔案的各個部分。

四、示例

public class Test implements Cloneable {
    private String name;
    public Test() {
    
    }
    
    public Test(String name) {
        this.name = name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
View Code

  說明:以上是一個很簡單和通用的類,下面我們的分析都將基於這個類。

  經過編譯後,得到class檔案,使用WinHex開啟,class檔案內容如下

  下面我們將從這個檔案的內容入手,慢慢分析class檔案各個部分。

五、class內容詳解

  5.1 magic

  class檔案的最開始4個位元組為magic(魔數),用來確定該class檔案能夠被虛擬機器接受。而在我們的class檔案中,我們可以看到最開始4個位元組是CAFEBABE。所有的class檔案的開始4個位元組都是CAFEBABE。

  5.2 minor_version && major_version

  主次版本號,會隨著Java技術的發展而變化,表示虛擬機器能夠處理的版本號。在magic之後的minor_version和major_version分別是0和52(52 = 3 * 16 + 4)。

  5.3 constant_pool_count && constant_pool

  常量池中常量表的數量和常量表,常量池中的每一項是常量表,具體的常量表包含類和介面相關的常量,存了很多字面量和符號引用。字面量主要包括了文字字串final常量符號引用包括:1. 類和介面的全限定名 2. 欄位的名稱和描述符 3. 方法的名稱和描述符。

  常量池中的專案包含如下型別:

  從上面的圖中我們可以知道,常量池中常量項(常量項都對應一個表)為23(23 = 1 * 16 + 7),值得注意的是常量項的索引值從1開始,到22,總共22項,索引值為0的項預留出來,暫時還未使用。緊接著就是常量項,每個常量項的第一個位元組u1表示標誌(tag),標誌(tag)表示是什麼型別的專案,標誌的值為上表給出的值,如標誌為1(tag = 1),表示CONSTANT_Utf8_info專案。上圖中的第一個常量項為的標誌tag的值為10(10 = 0 * 16 + A),為CONSTANT_Methodreef_info表,表示類中方法的符號引用。其中CONSTANT_Methodref_info表的結構如下

  接著,在tag後面是u2型別的index專案,為4(4 = 0 * 16 + 4),表示指向常量池的第四項,由描述可知,第四項應該是CONSTANT_Class_info項,接著,又是u2型別的index專案,為18(18 = 1 * 16 + 2),表示指向常量池的第18項,由表的描述可知,第十八項應該是CONSTANT_NameAndType_info項,正確性我們之後進行驗證。第一個常量項CONSTANT_Methodref_info就完了。

  緊接著第一個常量項是第二個常量項,tag為9(0 * 16 + 9),表示CONSTANT_Fieldref_info表,表示欄位的符號引用。CONSTANT_Field_info的表結構如下

  接著,在tag後面的是u2型別的index專案,為3(0 * 16 + 3),表示指向常量池的第三項,應該為CONSTANT_Class_info項,緊接著是index專案,為19(1 * 16 + 3),表示指向常量池的第19項,應該是CONSTANT_NameAndType_info項。

  接著,是第三項常量,tag為7(0 * 16 + 7),表示CONSTANT_Class_info表,其中,其表結構如下

  

  接著tag的為型別為u2的index,為20(1 * 16 + 4),表示指向常量池的第二十項,表示全限定名。

  接著第四項常量,tag為7(0 * 16 + 7),表示CONSTANT_Class_info表,表結構已經介紹了,接著是u2的index,為21(1 * 16 + 5),表示指向全限定名。

  接著第五項常量,tag為7,u2的型別的index為22(1 * 16 + 6),表示指向全限定名。

  同理,按照這樣的方法進行分析,最後給出一個總的常量池表如下。

 

  說明:#表示常量項的索引,Utf8表中存放的是具體的字串。如#6中存放的就是字串name,#10中存放的就是字串Code,關於表示的具體含義,我們稍後會進行解釋。

  除去我們之前介紹的常量表結構,常量池中其他常量表的結構分別如下:

  

  

  說明:描述符分為欄位描述符和方法描述符,欄位描述符用來描述欄位的資料型別,方法的描述符用來描述方法的引數列表(包括數量、型別、順序)和返回值。基本型別和物件的描述符如下:

  說明:上表中並沒有指出出現陣列瞭如何描述,每一個維度使用一個前置的"["來描述,如int[]描述為[I,String[]描述為[Ljava/lang/String;long型別是使用字元J進行標識,物件型別是使用L字元進行標識。如String型別描述為Ljava/lang/String;short型別描述為S,對於方法描述符而言,按照先引數列表,後返回值進行描述,引數列表按照引數順序放在小括號"()"內部,如void inc(int i)描述為(I)V;int getName()描述為()I;void setName(String name)描述為(Ljava/lang/String)V;方法的描述符與方法名稱是分開進行的,方法描述中並沒有包含方法名。

  5.4 access_flags

  常量池後的兩個位元組,用於識別類或介面層次的訪問資訊,如,這個class是類或者是介面,是否為public,abstract,final等等。具體的標誌含義如下:

  

  說明:其中ACC_INTERFACE與ACC_FINAL不能同時存在。

  從之前的位元組碼中可以知道,access_flags為0x0021(0x0021 = 0x0020|0x0001),即為public,並且允許使用invokespecial位元組碼。

  5.5 this_class

  接著access_flags後面的u2型別的this_class,表示對常量池的索引,該索引項為CONSTANT_Class_info型別,從前面我們知道this_class為0x0003,表示對常量池第三項的索引,第三項我們知道確實是CONSTANT_Class_info型別,而第三項所表示的內容為Test,即表示當前類。

  5.6 super_class

  接著this_class後面的是u2型別的super_class,表示對常量池的索引,從前面我們知道super_class為0x0004,表示對常量池第四項的索引,第四項我們知道是CONSTANT_Class_info型別,而第四項所表示的內容為java/lang/Object,表示Test的父類為Object類。

  5.7 interfaces_count && interfaces

  接著super_class後面的u2型別表示介面數量,此介面數量為該類直接實現或者由介面所擴充套件介面的數量。從前面我們可以知道,interfaces_count為0x0001,表示介面數量為1,從程式中我們也可以知道確實是只實現了Cloneable介面。

  接著就是型別為u2的interfaces,表示對常量池的索引,值為0x0005,表示對第五項的索引,第五項為CONSTANT_Class_info型別,所表示的內容為java/lang/Cloneable,從源程式我們可以進行驗證。

  5.8 fields_count && fields

  接著interfaces後面的是型別為u2的fields_count(包括類變數和例項變數,不包括區域性變數),值為0x0001,為1,從源程式我們知道只宣告瞭一個例項變數name,所以為1。接著fields_count的是型別為fields_info表,field_info表的具體結構如下

  接著fields_count後的是field_info表,首先是u2型別的access_flags,access_flags的具體含義如下表所示

  說明:public、private、protected只能會有一個有效。final、volatile只能有一個有效。

  我們可以知道access_flags為0x0002,表示為private,緊接著是型別為u2的name_index,值為0x0006,表示對常量池第六項的索引,常量池第六項為Class_Utf8_info型別,內容為name,則表示了欄位的名稱。接著是型別為u2的descriptor_index,值為0x0007,表示對常量池第七項的索引,常量池第七項為Class_Utf8_info型別,內容為Ljava/lang/String,緊接著是型別為u2的attributes_count,為0x0000,表示field_info表沒有巢狀attribute_info表。

  最後的field_info表結構如下:

  5.9 methods_count && methods

  fields後面的是型別為u2的methods_count,methods_count的計數只包括在該類或介面中顯示定義的方法,不包括從超類或父介面繼承來的方法,我們可以知道methods_count的值為0x0004,表示有四個方法,從源程式我們也可以進行驗證。緊接著methods_count的是method_info表,method_info表的具體結構如下(與field_info完全相同)

  而對於access_flags標誌種類如下

  method_count為4表示接下來有4個method_info表。

  首先是第一個method_info表,u2型別的access_flags,為0x0001,表示public,接著是型別為u2的name_index,為0x0008,表示對常量池第八項的索引,第八項為Class_Utf8_info型別,內容為<init>,表示例項初始化方法,由編譯器產生;接著是型別為u2的descriptor_index,為0x0009,表示對常量池第九項的索引,第九項為Class_Utf8_info型別,內容為()V,表示引數為空,返回值為void,接著是型別為u2的attributes_count,為0x0001,表示有一個屬性表;接著是attribute_info表,attribute_info表的結構如下:

  接著attributes_count的是型別為u2的attribute_name_index,為0x000A,指向常量池第十項索引,第十項型別為Class_Utf8_info型別,內容為Code,Code屬性表示屬性的具體類別;接著是型別為u4的attribute_length,為0x00000021,表示屬性長度為33(2 * 16 + 1),接著就是具體每個屬性的info資訊,對於Code屬性而言,其結構如下

  接著attribute_length的是型別為u2的max_stack,為0x0001,表示運算元棧的最大深度,接著max_stack的是型別為u2的max_locals,為0x0001,表示區域性變數所需的儲存空間大小為1,區域性變數表的單位為slot,(byte、char、float、int、short、boolean等不超過32為的資料型別只佔據一個slot,double、long64為資料型別需要兩個slot),區域性變數表可以存放方法引數(例項方法的this引用)顯式處理器的引數catch中所定義的異常方法體中定義的區域性變數。接著max_locals的是型別為u4的code_length,為0x00000005,為5,表示code程式碼的長度為5,接著code的是型別為u2的exception_table_length,為0x0000,表示不存在異常表,接著是型別為u2的attributes_count(exception_table_length為0),為0x0001,為1,表示屬性數量為1,表示有一個屬性表,接著就是attribute_info表,型別為u2的attribute_name_index,為0x000B,表示對常量池第11項索引,第11項型別為Class_Utf8_info,內容為LineNumberTable,表示具體的屬性,LineNumberTable的具體結構如下圖所示

  接著attribute_name_index的是型別為u4的attribute_length,為0x0000000A,長度為10,表示屬性長度為10,接著attribute_length的是型別為u2的line_number_table_length,為0x0002,為2,表示有兩個line_number_info表,line_number_info表的具體結構如下:

  首先是第一個line_number_info表,型別為u2的start_pc,為0x0000,為0;接著是型別為u2的line_number,為0x0003,為3。第二個line_number_info表,型別為u2的start_pc,為0x0004,為4,接著是line_number,為0x0005,為5;

  至此,第一個method_info表就已經分析完了,第一個method_info表的包含結構如下圖所示。

  

  第二個、第三個、第四個Method_info都可以按照第一個Method_info表的方法進行類推。最後的4個表的說明如下

  

  

  除了上面介紹的屬性表之外,還有其他的屬性表,下面進行介紹。

  5.10 attributes_count && attributes

  接在methods後面的是attributes_count,attributes_count為0x0001,表示有一個attribute_info表,接著attribute_count後面的是attribute_name_index,為0x0010,表示指向常量池第16項的索引,第16項型別為Class_Utf8_info型別,內容為SourceFile,表示屬性為SourceFile,SourceFile屬性的具體結構如下

  可以看到attributes_count為0x0001,為1,表示有一個屬性表,緊接著,attribute_name_index為0x0010,為16,對應常量池第十六項,型別為Class_Utf8_info,內容為SourceFile,型別為u4的attribute_length,為0x00000002,值為2,緊接著是型別為u2的sourcefile_index,為0x0011,為17。

  至此,整個class檔案都已經解析完成了,其實經過分析,我們發現其實分析class檔案並不困難,都有固定的格式。

六、特殊字串

  常量池容納的符號引用包括三種特殊的字串:全限定名、簡單名稱、描述符。全限定名為類或介面的全限定名,如java.lang.Object物件的全限定名為java/lang/Object,用/代替.即可。簡單名稱為欄位名或方法名的簡單名稱,如Object物件的toString()方法的簡單名稱為toString,描述符我們在之前已經介紹過了。

七、指令介紹

  7.1 方法呼叫指令:

  1. invokevirtual,用於呼叫物件的例項方法,根據物件的實際型別進行分派。

  2. invokeinterface,用於呼叫介面方法,在執行時搜尋一個實現了該介面方法的物件,找出合適的方法進行呼叫。

  3. invokespecial,用於呼叫需要特殊處理的例項方法,包括例項初始化方法、私有方法、父類方法。

  4. invokestatic,用於呼叫類方法,static方法。

  5. invokedynamic,用於在執行時動態解析出呼叫點限定符所引用的方法,並執行該方法。

  7.2 返回指令

  ireturn(boolean、byte、char、short、int)、lreturn、freturn、dreturn、areturn(返回為物件引用型別)、return(返回為void)

  7.3 同步指令

  虛擬機器支援方法級的同步和方法內部一段指令序列的同步,都使用管程(Monitor)來支援。方法級(synchronized修飾)同步時隱式的,無需通過位元組碼指令來控制,方法呼叫時檢查ACC_SYNCHRONIZED標誌。方法內部的synchronized語句塊使用monitorenter,monitorexit指令來確保同步。

七、總結

  class檔案看似很複雜,其實經過分析我們發現class檔案並不難,通過分析class檔案,我們知道了源程式經過編譯器編譯之後如何組織在class檔案中,進而為虛擬機器執行程式提供搭起了橋樑。也相信經過分析,讀者也能夠分析class檔案了,那麼我們的目的也就達到了,謝謝各位園友的觀看~

  

 

  

  

  

相關文章