Class文件結構

大家都知道,Java之所以如此受人喜歡,很大的原因是要規於它的跨平臺性。“一次編寫,到處運行”,Java誕生之時曾提出的著名的宣傳口號,充分表達了軟件開發人員對衝破平臺界限的渴求。

或許大部分程序員都認爲Java虛擬機執行Java程序是一件理所當然和天經地義的事,但時至今日,商業機構和開源機構已經在Java語言之外發展出一大批在Java虛擬機之上運行的語言,如Clojure、Groovy、JRuby、Jython、Scale等。使用Java編譯器可以把Java代碼編譯爲存儲字節碼的Class文件,使用JRuby等其它語言的編譯器一樣可以把程序代碼編譯成Class文件,Java之所以能夠跨平臺運行,是因爲Java虛擬機可以載入和執行同一種平臺無關的字節碼。也就是說,實現語言平臺無關性的基礎是虛擬機和字節碼存儲格式,虛擬機並不關心Class的來源是什麼語言,只要它符合Class文件應有的結構就可以在Java虛擬機中運行。

 

Class類文件的結構 

  Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部都是程序運行的必要數據,沒有空隙存在。當遇到需要佔用8位字節以上的空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。

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

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

表是由多個無符號數或者其它表作爲數據項構成的複合數據類型,所有表都習慣性地以"_info"結尾。表用於描述有層次關係的複合結構的數據,整個Class文件本質上就是一張表。 它由下表所示的數據項構成。

接下來我們根據表中的數據項來描述class文件的格式。

    1. 魔數 

    每個Class文件的頭4個字節稱爲魔數(Magic Number),它的唯一作用是用於確定這個文件是否爲一個能被虛擬機接受的Class文件。很多文件存儲標準中都使用魔數來進行身份識別,譬如圖片格式,如gif或jpeg等在文件頭中都存有魔數。Class文件魔數的值爲0xCAFEBABE。如果一個文件不是以0xCAFEBABE開頭,那它就肯定不是Java class文件。

    2. 版本號 

    緊接着魔數的4個字節存儲的是Class文件的版本號:第5和第6是次版本號(Minior Version),第7個和第8個字節是主版本號(Major Version)。Java的版本號是人45開始的,JDK1.1之後的每個JDK大版本發佈主版本號向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能運行以後版本的Class文件,即使文件格式並未發生變化。JDK1.1能支持版本號爲45.0~45.65535的Class文件,JDK1.2則能支持45.0~46.65535的Class文件。JDK1.7可生成的Class文件主版本號的最大值爲51.0。 

     

     3.常量池 

     緊接着魔數與版本號之後的是常量池入口,常量池是Class文件結構中與其它項目關聯最多的數據類型,也是佔用Class文件空間最大的數據項目之一,同時它還是在文件中第一個出現的表類型數據項目。由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值(constant_pool_count)。從1開始計數。第0項騰出來滿足後面某些指向常量池的索引值的數據在特定情況下需要表達"不引用任何一個常量池項目"的意思,這種情況就可以把索引值置爲0來表示。但儘管constant_pool列表中沒有索引值爲0的入口,缺失的這一入口也被constant_pool_count計數在內。例如,當constant_pool中有14項,constant_poo_count的值爲15。Class文件結構中只有常量池的容量計數是從1開始的,對於其他集合類型,包括接口索引集合、字段表集合、方法表集合等的容量計數都是從0開始的。

     常量池之中主要存放兩大類常量:字面量和符號引用。字面量比較接近於Java語言層面的常量概念,如文本字符串、被聲明爲final的常量值等。而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:

  •      類和接口的全限定名
  •      字段的名稱和描述符
  •      方法的名稱和描述符  

    Java代碼在進行Java編譯的時候,並不像C和C++那樣有"連接"這一步驟,而是在虛擬機加載Class文件的時候進行動態連接。也就是說,在Class文件中不會保存各個方法和字段的最終內存佈局信息,因此這些字段和方法的符號引用不經過轉換的話是無法被虛擬機使用的。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析並翻譯到具體的內存地址之中。 

    常量池中的每一項常量都是一個表,共有11種結構各不相同的表結構數據,這11種表都有一個共同的特點,就是表開始的第一位是一個u1類型的標誌位(tag,取值爲1至12,缺少標誌爲2的數據類型),代有當前對象屬於哪種常量類型,11常量類型所代表的具體含義如下表所示。

    

    說了這麼多,恐怕還是對常量池有點迷惑吧,我們舉個例子來看一下

    假如我們得到的Class文件的十六進制數的一段序列爲:

     

    第9位 16轉換爲十進制爲22,代表常量池中有21個常量。第10位的07帶表的是一個常量的tag值,可以從上表中看到,07代表CONSTANT_Class_info類型,從上表中可以看出,

CONSTANT_Class_info類型的結構有一個u1類型的tag,有一個u2類型的name_index,數量都是1。那麼可以看出接下來的第11位與第12位的值0002就是name_index的值。即指向的常量池中的第一個常量。第二項常量的標誌位爲0x01(看第13位),也就是CONSTANT_Utf_info類型,
CONSTANT_Utf_info類型有u2型的length與u1型的bytes。依此類推。

   如上所述,虛擬機加載Class文件的時候,就是這樣從常量池中得到相對應的數值。

   4.訪問標誌 

   緊接常量池後的兩個字節稱爲access_flags,它展示了文件中定義的類或接口的幾段信息。例如,訪問標誌指明文件中定義的是類還是接口;訪問標誌還定義的在類或接口的聲明中,使用了哪種修飾符oder和接口是抽象的,還是公共的;類的類型可以爲final,而final類不可能是抽象的;接口不能爲final類型的。這些標誌位的定義如下表所示:

    

    如一個TestClass類被public關鍵字修飾但沒有被聲明爲final和abstract,並且它使用了JDK1.2之後的編譯器進行編譯,因此它的ACC_PUBLIC、ACC_SUPER標誌應該爲真。因此它的access_flags的值應爲:0x0001|0x0020 = 0x0021。

    5. 類索引

    訪問標誌後面接下來的兩個字節是類索引(this_class),它是一個對常量池的索引。在this_class位置的常量池入口必須爲CONSTANT_Class_info表。該表由兩個部分組成——tag和name_index。tag部分是代表其的標誌位,name_index位置的常量池入口爲一個包含了類或接口全限定名的CONSTANT_Utf8_info表。 

    6.父類索引

    在class文件中,緊接在this_class之後是super_class項,它是一個兩個字節的常量池索引。在super_class位置的常量池入口是一個指向該類超類全限定名的CONSTANT_Class_info入口。因爲Java程序中所有對象的基類都是java.lang.Object類,除了Object類以外,常量池索引super_class對於所有的類均有效。對於Object類,super_class的值爲0。對於接口,在常量池入口super_class位置的項爲java.lang.Object

   7.interfaces_count和interfaces

   緊接着super_class的是interfaces_count,此項的含義爲:在文件中出該類直接實現或者由接口所擴展的父接口的數量。在這個計數的後面,是名爲interfaces的數組,它包含了對每個由該類或者接口直接實現的父接口的常量池索引。每個父接口都使用一個常量池中的CONSTANT_Class_info入口來描述,該CONSTANT_Class_info入口指向接口的全限定名。這個數組只容納那些直接出現在類聲明的implements子句或者接口聲明的extends子句中的父接口。超類按照在implements子句和extends子句中出現的順序在這個數組中顯現。 

   8. fields_count和fields

   在class文件中,緊接在interfaces後面的是對在該類或者接口中所聲明的字段的描述。首先是名爲fields_count的計數,它是類變量和實例變量的字段的數量總和。在這個計數後面的是不同長度的field_info表的序列(fields_count指出了序列中有多少個field_info表)。只有在文件中由類或者接口聲明瞭的字段才能在fields列表中列出。在fields列表中,不列出從超類或者父接口繼承而來的字段。另一方面,fields列表可能會包含在對應的Java源文件中沒有敘述的字段,這是因爲Java編譯器可以會在編譯時向類或者接口添加字段。

   在Java中,描述字段的信息有:字段的作用域、是實例變量還是類變量(static)、可變性(final)、併發可見性(volatile)、可否序列化(trasient)、字段數據類型、字段名稱。這些信息中,各個修飾符都是布爾值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。而字段叫什麼名字、字段被定義爲什麼數據類型,這些都是無法固定的,只能引用常量池中的常量來描述。

   

   字段修飾符放在access_flags項目中,它與類中的access_flags項目是非常相似的,都是一個u2的數據類型,其中可以設置 的標誌位和含義如下表所示

   

   跟隨access_flags標誌的是兩項索引值:name_index和descriptor_index。它們都是對常量池的引用,分別代表着字段的簡單名稱及字段和方法的描述符。描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。根據描述符規則,基本數據類型(byte、char、double、float、int、long、short、boolean)及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名來表示。

    

   對於數組類型,每一個維度將使用一個前置的"["字符來描述,如一個定義的"java.lang.String[][]"類型的二維數組,將被記錄爲:"[[Ljava/lang/String;",一個整型數組"int[]"將被記錄爲"[I"

   用描述符來描述方法時,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號"()"之內。如方法void inc()的描述符爲"()V",方法java.lang.String.toString()的描述符爲"()Ljava/lang/String;"。

    9.method_count和methods

    緊接着field後面的是對在該類或者接口中所聲明的方法的描述。其結構與fields一樣,不一樣的是訪問標誌。

    

    10.attributes_count和attributes

    class文件中最後的部分是屬性,它給出了在該文件類或者接口所定義的屬性的基本信息。屬性部分由attributes_count開始,attributes_count是指出現在後續attributes列表的attribute_info表的數量總和。每個attribute_info的第一項是指向常量池中CONSTANT_Utf8_info表的引引,該表給出了屬性的名稱。

    屬性有許多種。Java虛擬機規範定義了幾種屬性,但任何人都可以創建他們自己的屬性種類,並且把它們置於class文件中,Java虛擬機實現必須忽略任何不能識別的屬性。

    java虛擬機預設的9項虛擬機應當能識別的屬性如下表所示。

    

發佈了23 篇原創文章 · 獲贊 6 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章