1. 程式人生 > >關於Java虛擬機器二三事(五)---類檔案結構(上)

關於Java虛擬機器二三事(五)---類檔案結構(上)

1.前言

    當編寫完一段Java程式碼並儲存以後,其實Java程式碼會儲存在以.Java為副檔名作為結尾的檔案中,如test.java,而這個檔案若想在JVM上執行,則必須先利用javac編譯器進行編寫,形成所謂的“位元組碼(ByteCode)檔案”,即接下來要分析的重點內容---Class類檔案(.class檔案)。然後,JVM虛擬機器才會執行.class檔案。

    

    具體過程如上圖所示

    而Java虛擬機器想做的,並不僅僅只針對Java語言,因此,在設計JVM之初,設計者便決定Java虛擬機器將不和包括Java在內的任何語言繫結,Java虛擬機器只和位元組碼檔案(Class檔案,一種特定的二進位制檔案格式)所關聯。從而,體現了Java虛擬機器的一個重要特性——語言無關性。

    

    由次可見,無論採用何種程式語言,只需要有對應的編譯器將程式碼檔案編譯成統一的位元組碼檔案,便可以在java虛擬機器上執行。

2.Class類檔案結構

    由上圖可知,所有編譯器編譯之後的最終結果是位元組碼檔案,即Class檔案,因此,本節就對Class檔案進行講解。

2.1 Class類檔案的定義:

        Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料項嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符。這樣的安排可以使得整個Class檔案中儲存的內容幾乎全部是程式執行的必要資料,沒有空隙存在。

2.2 Class類檔案的內部結構

        根據Java虛擬機器規範的規定,Class檔案格式採用一種類似於C語言結構體的微結構來儲存資料,這種偽結構中只有兩種資料型別:無符號數和表。

        無符號數屬於基本的資料型別,以u1,u2,u4,u8來分別代表1個位元組,2個位元組,4個位元組和8個位元組,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成的字串值。

        表是由多個無符號數或者其他表作為資料項構成的複合資料型別,所有表都習慣性地以info結尾,表用於描述有層次關係的複合結構的資料,整個Class檔案本質上就是一張表,表結構如下所示。

類      型名    稱   數      量備     注
u4magic1魔數
u2minor_version1次版本號
u2mmajor_version1主版本號
u2constant_poolconstant_pool_count-1常量池數量
cp_infoconstant_pool1常量池的集合內容
u2access_flags1訪問控制符
u2this_class1類索引
u2super_class1父類索引
u2interfaces_count1介面索引數量
u2interfacesinterfaces_count介面數量
u2fields_count1欄位表集合數量
field_infofieldsfields_count欄位表集合內容
u2methods_count1方法表集合數量

method_info

methods

methods_count

方法表集合

u2aattributes_count1屬性表集合數量
attribute_infoattributesattributes_count屬性表集合內容

        這裡強調一點,Class的結構不同於XML等描述語言,由於它沒有任何分隔符號,所以在上表中的資料項,無論是數量還是順序,甚至於資料儲存的位元組序這樣的細節都被嚴格限定,哪個位元組代表什麼含義,長度是多少,先後順序如何,都不允許被改變。

2.2.1 魔數

        每個Class檔案的頭4個位元組稱為魔數(Magic Number),它的唯一作用就是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。很多檔案儲存標準中,都使用魔數進行身份識別,這裡需要注意的是,用魔數來代替副檔名進行識別主要是基於安全方面的考慮,因為檔案的副檔名可以隨意改動,而檔案格式的制定者可以自由地選擇魔數,只要這個魔數還沒有被廣泛採用過同時又不會引起混淆即可。例如,Class檔案的魔數就是0xCAFEBABE。

2.2.2 Class檔案版本

        魔數之後的四個位元組,分別儲存Class檔案的版本號:第5和第6位元組儲存次版本號(Minor Version),第7和第8位元組儲存主版本號(Major Version)。Java版本號是從45開始,高版本的JDK能向下相容以前版本的Class檔案,但不能執行以後版本的Class檔案,即使檔案格式並未發生任何變化,虛擬機器也必須拒絕執行超過其版本號的Class檔案。

2.2.3 常量池

        緊接著主次版本號之後的是常量池入口,常量池可以理解為Class檔案之中的資源倉庫,踏實Class檔案與其他專案關聯最多的資料型別,也是佔Class檔案空間最大的資料專案之一,同時,它還是在Class檔案中第一個出現的表資料型別專案。


        由於常量池中的常量數是不確定的,因此常量池入口處需要放置一個u2型別的資料,代表常量池容量計數值constant_pool_count,其實就是統計一下常量池裡有多少個常量),這裡值得注意的是,這個容量計數從1而非從0開始,因此,常量池的實際容量應該是constant_pool_count-1。

        例如常量池容量為0x0016,即十進位制的22,代表常量池中有21項常量。索引值範圍為1~21,。

        而設計者將第0項常量空出來,是有特殊考慮的,這樣做的目的在於滿足後面某些指向常量池的索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的含義,這種情況下就可以把索引值置為0來表示。

        常量池計數值之後,緊跟著的就是常量池內描述各項常量的表。常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。字面量比較接近於Java語言層面的常量概念,例如文字字串、final常量值等。而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:

            1. 類和介面的全限定名(Fully Qualified Name)

            2. 欄位的名稱和描述符(Descriptor)

            3. 方法的名稱和描述符

        Java程式碼在進行編譯的時候,區別於C/C++,它並沒有“連線”這一步驟,而是在虛擬機器載入Class檔案的時候進行動態連線,也就是說,在Class檔案中不會儲存各個方法、欄位的最終記憶體佈局資訊,此案次這些欄位、方法的符號引用不經過執行期轉換的話無法得到真正的記憶體入口地址,也就無法被虛擬機器直接使用。

        當虛擬機器執行時,需要從常量池獲得對應的符號引用,再在類建立或執行時對符號引用進行解析、翻譯到具體的記憶體地址之中。

        常量池中的每一項常量都是一個表,在JDK1.7之前一共有11種結構各不相同的表結構資料,在JDK1.7中為了更好地支援動態語言呼叫,又額外增加了三種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info)。

        這14種表都有一個共同特點,就是表開始的第一位是一個u1型別的標誌位(Tag),代表當前這個常量資料哪種常量型別。這14種常量型別所代表的的具體含義如下所示。


        綜上,常量池的組織可以說比較簡單清晰,即前端2個位元組用來表示常量池計數器(constant_pool_count),它用於記錄常量池的組成元素——常量池項的個數。


由上圖可知,cp_info(常量池項)的資料結構為表,資料型別就是上表中的所有型別。


所以,我們可以將cp_info進一步替換成具體的表型別,如下所示。

2.2.4 訪問標誌、類索引、父類索引、介面索引集合

    在常量池結束之後,緊跟著的就是Class檔案中的訪問標誌、類索引、父類索引以及介面索引集合。具體如下所示。

 2.2.4.1. 訪問標誌

        訪問標誌(access_flag)緊接著常量池,佔用2個位元組,也就是16位,如下圖所示。訪問標誌用於識別一些類或者介面層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public型別;是否定義為abstract型別;如果是類的話,是否被宣告為final等。

        當JVM在編譯某個類或者介面的原始碼時,JVM會解析這個類或者介面的訪問標誌資訊,然後將這些標誌設定到訪問標誌(access_flag)著16個位上,JVM會參考如下訪問表示資訊。

        a. 我們知道,每個定義的類或者介面都會生成class檔案,這裡也包括一些內部類,在某個類中定義的靜態內部類也會單獨生成一個class檔案。

        對於定義的類,JVM在將其編譯成class檔案時,會將class檔案的訪問標誌的11設定為1,該位叫做ACC_SUPER標誌位

        對於定義的介面,JVM在將其編譯成class檔案時,會將class檔案的訪問標誌的8設定為1,該位叫做ACC_INTERFACE標誌位  

        b. class檔案表示的類或者介面的訪問淺顯有public型別和包package型別。

        如果累或者介面被宣告為public型別的,那麼JVM在將其編譯成class檔案時,會將class檔案的訪問標誌的第16設定為1,該位叫做ACC_PUBLIC標識位

        c. 類是否為抽象型別的,即我們定義的類是否被abstract關鍵字所修飾。

        如果,我們宣告如下類:

  1. publicabstractclass MyClass{......}   

        那麼根據上文a,b所提及的內容可知,JVM將MyClass這個Java類編譯成class檔案的時候,會將class檔案的訪問標誌的第7位設定為1,第7位叫做ACC_ABSTRACT標誌位,同時,如果JVM編譯的物件是介面的話,也會將class檔案的class檔案的訪問標誌的第7位設定為1

        d. 該類是否聲明瞭final型別,即表示此類不能用於繼承。

        此時,JVM會在編譯class檔案的過程中,將class的訪問標誌的第12位設定為1。第12位叫做ACC_FINAL標誌位。

e. 如果我們這個class檔案不是JVM通過Java原始碼檔案編譯而成的,而是使用者自己通過class檔案的組織規則生成的,那麼一般會對class檔案的訪問標誌的第4位設定為1,通過JVM編譯原始碼產生的class檔案此標誌位為0,第4位叫做ACC_SYTHETIC標誌位。

f. 列舉類,如果對於定義的列舉類,形如

    public enum EnumTest{....}

        JVM也會將此列舉類編譯成class檔案,這時,對於這樣的class檔案,JVM會對訪問標誌的第2位設定為1,以表示它是一個列舉類,第2位叫做ACC_ENUM標誌位

g. 註解類,對於定義的註解類,如public @interface{......},JVM會對此註解類編譯成class檔案,對於這樣的class檔案,JVM會將訪問標誌第3位設定為1,以表示這個類為註釋類,第3位叫做ACC_ANNOTATION標誌位

        當JVM確定了上述標誌位的值以後,就可以確定訪問標誌(access_flag的值了,實際上,JVM上述標誌會根據上述確定的標誌位的值,對這些標誌位的值取或,便得到了訪問標誌(access_flag)。


舉例:定義一個最簡單的類Simple.java,使用編譯器編譯成class檔案,然後觀察class檔案中的訪問標誌的值,以及使用javap -v Simple 檢視訪問標誌。

  1. package com.louis.jvm;  
  2. publicclass Simple {  
  3. }  

使用UltraEdit檢視編譯成的class檔案,如下圖所示:

上述的圖中黃色部分表示的是常量池部分,具體為什麼是常量池部分不是本文的重點,有興趣的讀者可以參考我的《Java虛擬機器原理圖解》系列關於常量池的部落格,你就可以很輕鬆地識別常量它們了。

常量池後面緊跟著就是訪問標誌,它的十六進位制值為0x0021,二進位制的值為:00000000 00100001,由二進位制的1的位數可以得出第11、16位為1,分別對應ACC_SUPER標誌位ACC_PUBLIC標誌位

也可以通過一下運算:

          0x0021 = 0x0001 | 0x0020,  即:   訪問標誌表示的標誌是ACC_PUBLIC + ACC_SUPER

為了驗證我們的運算,使用javap -v Simple檢視反編譯資訊如下:(小技巧:使用javap -v Simple指令的結果展示在命令提示符下顯示不友好,一般我是使用javap -v Simple > temp.txt,將結果重定向到檔案中,然後檢視檔案)

2.2.4.2. 類索引、父類索引與介面索引集合

1. 類索引
        我們知道,一般情況下一個Java類原始檔經過JVM編譯會生成一個class檔案,也有可能一個Java類原始檔中定義了其他類或者內部類,這樣編譯出來的class檔案就不止一個,但每一個class檔案表示某一個類,值域這個class表示哪個類,就由“類索引”這個資料項來確定。JVM通過類的完全限定名確定是某一個類。
        類索引的作用,就是為了指出class檔案所描述的這個類叫什麼名字。
類所有緊接著訪問標誌的後面,佔有2個位元組,在這兩個位元組中儲存的值是一個指向常量池的一個索引,該索引指向的是CONSTANT_Class_info常量池項。

        以上面定義的Simple.class 為例,如下圖所示,檢視他的類索引在什麼位置和取什麼值。

          

         由上可知,它的類索引值為0x0001,那麼,它指向了常量池中的第一個常量池項,那我們再看一下常量池中的資訊。使用javap -v Simple,常量池中有以下資訊:

         

        可以看到常量池中的第一項是CONSTANT_Class_info,它表示一個“com/louis/jvm/Simple”的類名。即類索引告訴我們這個class檔案所表示的是哪一個類。

2. 父類索引

        java支援單繼承模式,除了java.lang.Object類之外,每一個類有且只有一個父類。class檔案中緊接著類索引(this_class)之後的兩個位元組區域表示父類索引,跟類索引一樣,父類索引這兩個位元組中的值指向了常量池中的某個常量池項CONSTANT_Class_info,表示該class是繼承自哪一個類

3. 介面索引集合

        一個類可以不實現任何介面,也可以實現很多介面,為了表示當前類實現的介面資訊,class檔案使用瞭如下結構體描述某個類的介面實現資訊:


        由於類實現的介面數目不確定,因此介面索引集合的描述的前部分叫介面計數器(interfaces_count),介面計數器佔兩個位元組,其中的值表示這個類實現了多少個介面,緊跟著介面計數器的部分就是介面索引部分了,每一個介面索引佔有2個位元組,介面計數器的值代表著後面跟著的介面索引的個數,介面索引和類索引和父類索引一樣,其內的值儲存指向了常量池中的常量池項的索引,表示這個介面的全限定名。

舉例:

      定義一個Worker介面,然後類Programmer實現這個Worker介面,然後我們觀察Programmer的介面索引集合是怎樣表示的。

  1. /** 
  2.  * Worker 介面類 
  3.  * @author luan louis 
  4.  */
  5. publicinterface Worker{  
  6.     publicvoid work();  
  7. }  
  1. package com.louis.jvm;  
  2. publicclass Programmer implements Worker {  
  3.     @Override
  4.     publicvoid work() {  
  5.         System.out.println("I'm Programmer,Just coding....");  
  6.     }  
  7. }