JVM之.class檔案解析

2020-08-10 14:43:32

目錄

1 概述

2 檔案結構

3 範例分析


1 概述

Java位元組碼類檔案(.class)是Java編譯器編譯Java原始檔(.java)產生的「目標檔案」。它是一種8位元位元組的二進制流檔案, 各個數據項按順序緊密的從前向後排列, 相鄰的項之間沒有間隙, 這樣可以使得class檔案非常緊湊, 體積輕巧, 可以被JVM快速的載入至記憶體, 並且佔據較少的記憶體空間(方便於網路的傳輸)。

Java原始檔在被Java編譯器編譯之後, 每個類(或者介面)都單獨佔據一個class檔案, 並且類中的所有資訊都會在class檔案中有相應的描述, 由於class檔案很靈活, 它甚至比Java原始檔有着更強的描述能力。

class檔案中的資訊是一項一項排列的, 每項數據都有它的固定長度, 有的佔一個位元組, 有的佔兩個位元組, 還有的佔四個位元組或8個位元組, 數據項的不同長度分別用u1, u2, u4, u8表示, 分別表示一種數據項在class檔案中佔據一個位元組, 兩個位元組, 4個位元組和8個位元組。 可以把u1, u2, u3, u4看做class檔案數據項的「型別」 。

JVM是一種規範,人們常說Java是跨平臺的語言,而JVM幫助遮蔽了不同操作系統的底層,是跨語言的平臺.它不僅僅應用於Java語言,它是可以認識所有能編譯成.class格式的檔案(位元組碼檔案).

2 檔案結構

一個典型的class檔案分爲:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods 和Attributes這十個部分,用一個數據結構可以表示如下:

Class檔案是一組以8位元位元組爲基礎的二進制流,各個數據專案按照嚴格順序緊湊排列在Class檔案中。
所有的16位元,32位元,64位元長度的數據將被構造成2個,4個,8個位元組單位來標示。

總體格式表:

型別 名稱 數量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attribute_count 1
attribute_info attributes attributes_count

class檔案中的資訊是一項一項排列的, 每項數據都有它的固定長度, 有的佔一個位元組, 有的佔兩個位元組, 還有的佔四個位元組或8個位元組, 數據項的不同長度分別用u1, u2, u4, u8表示, 分別表示一種數據項在class檔案中佔據1個位元組, 2個位元組, 4個位元組和8個位元組。 可以把u1, u2, u3, u4看做class檔案數據項的「型別」 。

class格式說明

1、magic
在class檔案開頭的四個位元組, 存放着class檔案的魔數, 這個魔數是class檔案的標誌,他是一個固定的值: 0XCAFEBABE 。 也就是說他是判斷一個檔案是不是class格式的檔案的標準, 如果開頭四個位元組不是0XCAFEBABE, 那麼就說明它不是class檔案, 不能被JVM識別。

2、minor_version 和 major_version
緊接着魔數的四個位元組是class檔案的此版本號和主版本號。
隨着Java的發展, class檔案的格式也會做相應的變動。 版本號標誌着class檔案在什麼時候, 加入或改變了哪些特性。 舉例來說, 不同版本的javac編譯器編譯的class檔案, 版本號可能不同, 而不同版本的JVM能識別的class檔案的版本號也可能不同, 一般情況下, 高版本的JVM能識別低版本的javac編譯器編譯的class檔案, 而低版本的JVM不能識別高版本的javac編譯器編譯的class檔案。 如果使用低版本的JVM執行高版本的class檔案, JVM會拋出java.lang.UnsupportedClassVersionError 。具體的版本號變遷這裏不再討論, 需要的讀者自行查閱資料。

3、constant_pool
在class檔案中, 位於版本號後面的就是常數池相關的數據項。 常數池是class檔案中的一項非常重要的數據。 常數池中存放了文字字串, 常數值, 當前類的類名, 欄位名, 方法名, 各個欄位和方法的描述符, 對當前類的欄位和方法的參照資訊, 當前類中對其他類的參照資訊等等。 常數池中幾乎包含類中的所有資訊的描述, class檔案中的很多其他部分都是對常數池中的數據項的參照,比如後面要講到的this_class, super_class, field_info, attribute_info等, 另外位元組碼指令中也存在對常數池的參照, 這個對常數池的參照當做位元組碼指令的一個運算元。此外,常數池中各個項也會相互參照。

常數池是一個類的結構索引,其它地方對「物件」的參照可以通過索引位置來代替,我們知道在程式中一個變數可以不斷地被呼叫,要快速獲取這個變數常用的方法就是通過索引變數。這種索引我們可以直觀理解爲「記憶體地址的虛擬」。我們把它叫靜態池的意思就是說這裏維護着經過編譯「梳理」之後的相對固定的數據索引,它是站在整個JVM(進程)層面的共用池。

class檔案中的項constant_pool_count的值爲1, 說明每個類都只有一個常數池。 常數池中的數據也是一項一項的, 沒有間隙的依次排放。常數池中各個數據項通過索引來存取, 有點類似與陣列, 只不過常數池中的第一項的索引爲1, 而不爲0, 如果class檔案中的其他地方參照了索引爲0的常數池項, 就說明它不參照任何常數池項。class檔案中的每一種數據項都有自己的型別, 相同的道理,常數池中的每一種數據項也有自己的型別。 常數池中的數據項的型別如下表:

每個數據項叫做一個XXX_info項,比如,一個常數池中一個CONSTANT_Utf8型別的項,就是一個CONSTANT_Utf8_info 。除此之外, 每個info項中都有一個標誌值(tag),這個標誌值表明瞭這個常數池中的info項的型別是什麼, 從上面的表格中可以看出,一個CONSTANT_Utf8_info中的tag值爲1,而一個CONSTANT_Fieldref_info中的tag值爲9 。

Java程式是動態鏈接的, 在動態鏈接的實現中, 常數池扮演者舉足輕重的角色。 除了存放一些字面量之外, 常數池中還存放着以下幾種符號參照:
(1) 類和介面的全限定名
(2) 欄位的名稱和描述符
(3) 方法的名稱和描述符
我們有必要先瞭解一下class檔案中的特殊字串, 因爲在常數池中, 特殊字串大量的出現,這些特殊字串就是上面說的全限定名和描述符。

4、access_flag 儲存了當前類的存取許可權

5、this_cass 儲存了當前類的全侷限定名在常數池裏的索引

6、super class 儲存了當前類的父類別的全侷限定名在常數池裏的索引

7、interfaces 儲存了當前類實現的介面列表,包含兩部分內容:interfaces_count 和interfaces[interfaces_count]
interfaces_count 指的是當前類實現的介面數目
interfaces[] 是包含interfaces_count個介面的全侷限定名的索引的陣列

8、fields 儲存了當前類的成員列表,包含兩部分的內容:fields_count 和 fields[fields_count]
fields_count是類變數和範例變數的欄位的數量總和。
fileds[]是包含欄位詳細資訊的列表。

9、methods 儲存了當前類的方法列表,包含兩部分的內容:methods_count和methods[methods_count]
methods_count是該類或者介面顯示定義的方法的數量。
method[]是包含方法資訊的一個詳細列表。

10、attributes 包含了當前類的attributes列表,包含兩部分內容:attributes_count 和 attributes[attributes_count]
class檔案的最後一部分是屬性,它描述了該類或者介面所定義的一些屬性資訊。attributes_count指的是attributes列表中包含的attribute_info的數量。
屬性可以出現在class檔案的很多地方,而不只是出現在attributes列表裏。如果是attributes表裏的屬性,那麼它就是對整個class檔案所對應的類或者介面的描述;如果出現在fileds的某一項裡,那麼它就是對該欄位額外資訊的描述;如果出現在methods的某一項裡,那麼它就是對該方法額外資訊的描述。

3 範例分析

上面大致講解了一下class檔案的結構,這裏,我們拿一個class檔案做一個簡單的分析,來驗證上面的檔案結構是否確實是如此。

我們在這裏新建一個java檔案,Hello.java,具體內容如下:

public class Hello{
    private int test;
    public int test(){
        return test;
    }
}

然後再通過javac命令將此java檔案編譯成class檔案:

javac /d/class_file_test/Hello.java

編譯之後的class檔案十六進制結果如下所示,可以用UltraEdit等十六進制編輯器開啓,得到:

接下來我們就按照class檔案的格式來分析上面的一串數位,還是按照之前的順序來:

  • magic:
    CA FE BA BE ,代表該檔案是一個位元組碼檔案,我們平時區分檔案型別都是通過後綴名來區分的,不過後綴名是可以隨便修改的,所以僅靠後綴名不能真正區分一個檔案的型別。區分檔案型別的另個辦法就是magic數位,JVM 就是通過 CA FE BA BE 來判斷該檔案是不是class檔案

  • version欄位
    00 00 00 34,前兩個位元組00是minor_version,後兩個位元組0034是major_version欄位,對應的十進制值爲52,也就是說當前class檔案的主版本號爲52,次版本號爲0。下表是jdk 1.6 以後對應支援的 Class 檔案版本號:

常數池,constant_pool:
3.1. constant_pool_count
緊接着version欄位下來的兩個位元組是:00 12代表常數池裏包含的常數數目,因爲位元組碼的常數池是從1開始計數的,這個常數池包含17個(0x0012-1)常數。

3.2.constant_pool
接下來就是分析這17個常數:

3.2.1. 第一個變數 0a 00 04 00 0e
首先,緊接着constant_pool_count的第一個位元組0a(tag=10)根據上面的表格(常數池的型別圖)

  ![image_1c2tj6ib6pslkbb1876ot81rjj4v.png-4kB][7]

可知,這表示的是一個CONSTANT_Methodref。CONSTANT_Methodref的結構如下:
CONSTANT_Methodref_info {
    u1 tag;    //u1表示佔一個位元組
    u2 class_index;    //u2表示佔兩個位元組
    u2 name_and_type_index;    //u2表示佔兩個位元組
}

其中class_index表示該方法所屬的類在常數池裏的索引,name_and_type_index表示該方法的名稱和型別的索引。常數池裏的變數的索引從1開始。

那麼這個methodref結構的數據如下:

0a  //tag  10表示這是一個CONSTANT_Methodref_info結構
00 04 //class_index 指向常數池中第4個常數所表示的類
00 0e  //name_and_type_index 指向常數池中第14個常數所表示的方法

3.2.2. 第二個變數09 00 03 00 0F
接着是第二個常數,它的tag是09,根據上面的表格可知,這表示的是一個CONSTANT_Fieldref的結構,它的結構如下:

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

和上面的變數基本一致。

09 //tag
00 03 //指向常數池中第3個常數所表示的類
00 0f //指向常數池中第15個常數所表示的變數

3.2.3. 第三個變數 07 00 10

tag爲07表示是一個CONSTANT_Class變數,這個變數的結構如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

除了tag欄位以外,還有一個name_index的值爲00 10,即是指向常數池中第16個常數所表示的Class名稱。

3.2.4. 第四個變數07 00 11
同上,也是一個CONSTANT_Class變數,不過,指向的是第17個常數所表示的Class名稱。

3.2.5. 第五個變數 01 00 04 74 65 73 74
tag爲1,表示這是一個CONSTANT_Utf8結構,這種結構用UTF-8的一種變體來表示字串,結構如下所示:

                    CONSTANT_Utf8_info {
                                   u1 tag;
                                   u2 length;
                                   u1 bytes[length];
                    }

其中length表示該字串的位元組數,bytes欄位包含該字串的二進制表示。

01 //tag  1表示這是一個CONSTANT_Utf8結構
00 04 //表示這個字串的長度是4位元組,也就是後面的四個位元組74 65 73 74
74 65 73 74 //通過ASCII碼錶 碼表轉換後,表示的是字串「test」

接下來的8個變數都是字串,這裏就不具體分析了。

3.2.6. 第十四個常數 0c 00 07 00 08
tag爲0c,表示這是一個CONSTANT_NameAndType結構,這個結構用來描述一個方法或者成員變數。具體結構如下:

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

name_index表示的是該變數或者方法的名稱,這裏的值是0007,表示指向第7個常數,即是<init>

descriptor_index指向該方法的描述符的參照,這裏的值是0008,表示指向第8個常數,即是()V,由前面描述符的語法可知,這個方法是一個無參的,返回值爲void的方法。

綜合兩個欄位,可以推出這個方法是void <init>()。也即是指向這個NameAndType結構的Methodref的方法名爲void <init>(),也就是說第一個常數表示的是void <init>()方法,這個方法其實就是此類的預設構造方法。

3.2.7. 第十五個常數也是一個CONSTANT_NameAndType,表示的方法名爲「int test()」,第2個常數參照了這個NameAndType,所以第二個常數表示的是「int test()」方法。

3.2.8. 第16和17個常數也是字串,可以按照前面的方法分析。

3.3. 完整的常數池
最後,通過以上分析,完整的常數池如下:

          00 12  常數池的數目 18-1=17
          0a 00 04 00 0e  方法:java.lang.Ojbect void <init>()
          09 00 03 00 0f   方法 :Hello int test() 
          07 00 10  字串:Hello
          07 00 11 字串:java.lang.Ojbect
          01 00 04 74 65 73 74 字串:test
          01 00 01 49  字串:I
          01 00 06 3c 69 6e 69 74 3e 字串:<init>
          01 00 03 28 29 56 字串:()V
          01 00 04 43 6f 64 65 字串:Code 
          01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 字串:LineNumberTable 
          01 00 03 28 29 49 字串:()I
          01 00 0a 53 6f 75 72 63 65 46 69 6c 65 字串:SourceFile
          01 00 0a 48 65 6c 6c 6f 2e 6a 61 76 61 字串:Hello.java
          0c 00 07 00 08 NameAndType:<init> ()V
          0c 00 05 00 06 NameAndType:test I
          01 00 05 48 65 6c 6c 6f 字串:Hello
          01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 字串: java/lang/Object

通過這樣分析其實非常的累,我們只是爲了瞭解class檔案的原理纔來一步一步分析每一個二進制位元組碼。JDK提供了現成的工具可以直接解析此二進制檔案,即javap工具(在JDK的bin目錄下),我們通過javap命令來解析此class檔案:

javap -v -p -s -sysinfo -constants /d/class_file_test/Hello.class

解析得到的結果爲:

發現了沒有,上面生成程式碼中的Constant pool跟我們上面分析出來的完整常數池一模一樣,有木有!有木有?
這說明我們上面的分析的完成正確!

由此,我們終於弄懂了Constant pool的內幕。

接下來繼續看其他的欄位。

4.access_flag(u2)
00 21這兩個位元組的數據表示這個變數的存取標誌位,JVM對存取標示符的規範如下:

這個表裏面無法直接查詢到0021這個值,原因是0021=0020+0001,也就是表示當前class的access_flag是ACC_PUBLIC|ACC_SUPER。ACC_PUBLIC和程式碼裡的public 關鍵字相對應。ACC_SUPER表示當用invokespecial指令來呼叫父類別的方法時需要特殊處理。

5.this_class(u2) 00 03
this_class指向constant pool的索引值,該值必須是CONSTANT_Class_info型別,這裏是3,即指向常數池中的第三項,即是「Hello」。
 

6.super_class 00 04
super_class存的是父類別的名稱在常數池裏的索引,這裏指向第四個常數,即是「java/lang/Object」。
 

7.interfaces
interfaces包含interfaces_count和interfaces[]兩個欄位。因爲這裏沒有實現介面,所以就不存在interfces選項,所以這裏的interfaces_count爲0(0000),所以後面的內容也對應爲空。
 

8.fields

00 01 fields count        //表示成員變數的個數,此處爲1個
00 02 00 05 00 06 00 00   //成員變數的結構

每個成員變數對應一個field_info結構:

field_info {
    u2 access_flags; 0002
    u2 name_index; 0005
    u2 descriptor_index; 0006
    u2 attributes_count; 0000
    attribute_info attributes[attributes_count];
}

access_flags爲0002,即是ACC_PRIVATE
name_index指向常數池的第五個常數,爲「test」
descriptor_index指向常數池的第6個常數爲「I」
三個欄位結合起來,說明這個變數是"private int test"。
接下來的是attribute欄位,用來描述該變數的屬性,因爲這個變數沒有附加屬性,所以attributes_count爲0,attribute_info爲空。

9.methods
00 02 00 01 00 07 00 08 00 01 00 09 ...
最前面的2個位元組是method_count
method_count:00 02,爲什麼會有兩個方法呢?我們明明只寫了一個方法,這是因爲JVM 會自動生成一個<init>方法,這個是類的預設構造方法。

接下來的內容是兩個method_info結構:

method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

前三個欄位和field_info一樣,可以分析出第一個方法是「public void <init>()」

00 01 ACC_PUBLIC
00 07  <init>
00 08  V()

接下來是attribute欄位,也即是這個方法的附加屬性,這裏的attributes_count =1,也即是有一個屬性。
每個屬性的都是一個attribute_info結構,如下所示:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

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

這裏的attribute_name_index值爲0009,表示指向第9個常數,即是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_length表示attribute所包含的位元組數,這裏爲0000001d,即是39個位元組,不包含attribute_name_index和attribute_length欄位。
max_stack表示這個方法執行的任何時刻所能達到的運算元棧的最大深度,這裏是0001
max_locals表示方法執行期間建立的區域性變數的數目,包含用來表示傳入的參數的區域性變數,這裏是0001.
接下來的code_length表示該方法的所包含的位元組碼的位元組數以及具體的指令碼。
這裏的位元組碼長度爲00000005,即是後面的5個位元組 2a b7 00 01 b1爲對應的位元組碼指令的指令碼。
參照下表可以將上面的指令碼翻譯成對應的助記符:

               2a   aload_0    
               b7   invokespecial
               00   nop
               01   aconst_null
               b1   return

這即是該方法被呼叫時,虛擬機器所執行的位元組碼

接下來是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時,表示處理所有的異常,這個可以用來實現finally的功能。

不過,這段程式碼裡沒有例外處理,所以exception_table_length爲0000,所以我們不做分析。

接下來是該方法的附加屬性,attributes_count爲0001,表示有一個附加屬性。
attribute_name_index爲000a,指向第十個常數,爲LineNumberTable。這個屬性用來表示code陣列中的位元組碼和java程式碼行數之間的關係。這個屬性可以用來在偵錯的時候定位程式碼執行的行數。LineNumberTable的結構如下:

     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];
     }

前面兩個欄位分別表示這個attribute的名稱是LineNumberTable以及長度爲00000006。接下來的0001表示line_number_table_length,表示line_number_table有一個表項,其中start_pc爲 00 00,line_number爲 00 00,表示第0行程式碼從code的第0個指令碼開始。

後面的內容是第二個方法,具體就不再分析了。

10.attributes
最後剩下的內容是attributes,這裏的attributes表示整個class檔案的附加屬性,不過結構還是和前面的attribute保持一致。00 01表示有一個attribute。
Attribute結構如下:

          SourceFile_attribute {
               u2 attribute_name_index;
               u4 attribute_length;
               u2 sourcefile_index;
          }

attribute_name_index爲000c,指向第12個常數,爲SourceFile,說明這個屬性是Source
attribute_length爲00000002
sourcefile_index爲000d,表示指向常數池裏的第13個常數,爲Hello.java
這個屬性表明當前的class檔案是從Hello.java檔案編譯而來。