1. 程式人生 > >Java Class類檔案結構

Java Class類檔案結構

平臺無關性

Java是與平臺無關的語言,這得益於Java原始碼編譯後生成的儲存位元組碼的檔案,即Class檔案,以及Java虛擬機器的實現。不僅使用Java編譯器可以把Java程式碼編譯成儲存位元組碼的Class檔案,使用JRuby等其他語言的編譯器也可以把程式程式碼編譯成Class檔案,虛擬機器並不關心Class的來源是什麼語言,只要它符合一定的結構,就可以在Java中執行。Java語言中的各種變數、關鍵字和運算子的語義最終都是由多條位元組碼命令組合而成的,因此位元組碼命令所能提供的語義描述能力肯定會比Java語言本身更強大,這便為其他語言實現一些有別於Java的語言特性提供了基礎,而且這也正是在類載入時要進行安全驗證的原因。
 

Class 類檔案的結構

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

根據Java虛擬機器規範的規定,Class檔案格式採用一種類似於C語言結構體的偽結構來儲存,這種偽結構中只有兩種資料型別:無符號數和表。無符號數屬於基本資料型別,以u1、u2、u4、u8來分別代表1、2、4、8個位元組的無符號數。表是由多個無符號數或其他表作為資料項構成的符合資料型別,所有的表都習慣性地以“_info”結尾。

整個Class檔案本質上就是一張表,它由如下所示的資料項構成。

從表中可以看出,無論是無符號數還是表,當需要描述同一型別但數量不定的多個數據時,經常會在其前面使用一個前置的容量計數器來記錄其數量,而便跟著若干個連續的資料項,稱這一系列連續的某一型別的資料為某一型別的集合,如:fields_count個field_info表資料便組成了方法表集合。這裡需要注意的是:Class檔案中各資料項是按照上表的順序和數量被嚴格限定的,每個位元組代表的含義、長度、先後順序都不允許改變。
 

魔數與 Class 檔案的版本

每個Class檔案的頭4個位元組稱為魔數(magic),它的唯一作用是判斷該檔案是否為一個能被虛擬機器接受的Class檔案。它的值固定為0xCAFEBABE。緊接著magic的4個位元組儲存的是Class檔案的次版本號和主版本號,高版本的JDK能向下相容低版本的Class檔案,但不能執行更高版本的Class檔案。
 

常量池

major_version之後是常量池(constant_pool)的入口,它是Class檔案中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一。

    常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近於Java層面的常量概念,如文字字串、被宣告為final的常量值等。而符號引用總結起來則包括了下面三類常量:

類和介面的全限定名(即帶有包名的Class名,如:org.lxh.test.TestClass)
欄位的名稱和描述符(private、static等描述符)
方法的名稱和描述符(private、static等描述符)
    虛擬機器在載入Class檔案時才會進行動態連線,也就是說,Class檔案中不會儲存各個方法和欄位的最終記憶體佈局資訊,因此,這些欄位和方法的符號引用不經過轉換是無法直接被虛擬機器使用的。當虛擬機器執行時,需要從常量池中獲得對應的符號引用,再在類載入過程中的解析階段將其替換為直接引用,並翻譯到具體的記憶體地址中。

    這裡說明下符號引用和直接引用的區別與關聯:

符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到了記憶體中。
直接引用:直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經存在於記憶體之中了。
 

訪問標誌

在常量池結束之後,緊接著的2個位元組代表訪問標誌(access_flag),這個標誌用於識別一些類或介面層次的訪問資訊,包括:這個Class是類還是介面,是否定義為public型別,abstract型別,如果是類的話,是否宣告為final,等等。每種訪問資訊都由一個十六進位制的標誌值表示,如果同時具有多種訪問資訊,則得到的標誌值為這幾種訪問資訊的標誌值的邏輯或。
 

類索引,父類索引與介面索引集合

類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引集合(interfaces)則是一組u2型別的資料集合,Class檔案中由這三項資料來確定這個類的繼承關係。類索引、父類索引和介面索引集合都按照順序排列在訪問標誌之後,類索引和父類索引兩個u2型別的索引值表示,它們各自指向一個型別為COMNSTANT_Class_info的類描述符常量,通過該常量中的索引值找到定義在COMNSTANT_Utf8_info型別的常量中的全限定名字串。而介面索引集合就用來描述這個類實現了哪些介面,這些被實現的介面將按implements語句(如果這個類本身是個介面,則應當是extend語句)後的介面順序從左到右排列在介面的索引集合中。
 

欄位表集合

欄位表(field_info)用於描述介面或類中宣告的變數。欄位包括了類級變數或例項級變數,但不包括在方法內宣告的變數。欄位的名字、資料型別、修飾符等都是無法固定的,只能引用常量池中的常量來描述。下面是欄位表的最種格式:

 

    其中的access_flags與類中的access_flagsfei類似,是表示資料型別的修飾符,如public、static、volatile等。後面的name_index和descriptor_index都是對常量池的引用,分別代表欄位的簡單名稱及欄位和方法的描述符。這裡簡單解釋下“簡單名稱”、“描述符”和“全限定名”這三種特殊字串的概念。

    前面有所提及,全限定名即指一個事物的完整的名稱,如在org.lxh.test包下的TestClass類的全限定名為:org/lxh/test/TestClass,即把包名中的“.”改為“/”,為了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個“,”來表示全限定名結束。簡單名稱則是指沒有型別或引數修飾的方法或欄位名稱,如果一個類中有這樣一個方法boolean  get(int name)和一個變數private final static int m,則他們的簡單名稱則分別為get()和m。

    而描述符的作用則是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序等)和返回值的。根據描述符規則,詳細的描述符標示字的含義如下表所示:

 

    對於陣列型別,每一維度將使用一個前置的“[”字元來描述,如一個整數陣列“int [][]”將為記錄為“[[I”,而一個String型別的陣列“String[]”將被記錄為“[Ljava/lang/String”

    用方法描述符描述方法時,按照先引數後返回值的順序描述,引數要按照嚴格的順序放在一組小括號內,如方法 int getIndex(String name,char[] tgc,int start,int end,char target)的描述符為“(Ljava/lang/String[CIIC)I”。

    欄位表包含的固定資料專案到descriptor_index為止就結束了,但是在它之後還緊跟著一個屬性表集合用於儲存一些額外的資訊。比如,如果在類中有如下欄位的宣告:staticfinalint m = 2;那就可能會存在一項名為ConstantValue的屬性,它指向常量2。關於attribute_info的詳細內容,在後面關於屬性表的專案中會有詳細介紹。

    最後需要注意一點:欄位表集合中不會列出從父類或介面中繼承而來的欄位,但有可能列出原本Java程式碼中不存在的欄位。比如在內部類中為了保持對外部類的訪問性,會自動新增指向外部類例項的欄位。
 

方法表集合

方法表(method_info)的結構與屬性表的結構相同,不過多贅述。方法裡的Java程式碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性裡,關於屬性表的專案,同樣會在後面詳細介紹。

    與欄位表集合相對應,如果父類方法在子類中沒有被覆寫,方法表集合中就不會出現來自父類的方法資訊。但同樣,有可能會出現由編譯器自動新增的方法,最典型的便是類構造器“<clinit>”方法和例項構造器“<init>”方法。

    在Java語言中,要過載一個方法,除了要與原方法具有相同的簡單名稱外,還要求必須擁有一個與原方法不同的特徵簽名,特徵簽名就是一個方法中各個引數在常量池中的欄位符號引用的集合,也就是因為返回值不會包含在特徵簽名之中,因此Java語言裡無法僅僅依靠返回值的不同來對一個已有方法進行過載。
 

屬性表集合

屬性表(attribute_info)在前面已經出現過多系,在Class檔案、欄位表、方法表中都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。

    屬性表集合的限制沒有那麼嚴格,不再要求各個屬性表具有嚴格的順序,並且只要不與已有的屬性名重複,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性資訊,但Java虛擬機器執行時會忽略掉它不認識的屬性。Java虛擬機器規範中預定義了9項虛擬機器應當能識別的屬性(JDK1.5後又增加了一些新的特性,因此不止下面9項,但下面9項是最基本也是必要,出現頻率最高的),如下表所示:

    對於每個屬性,它的名稱都需要從常量池中引用一個CONSTANT_Utf8_info型別的常量來表示,每個屬性值的結構是完全可以自定義的,只需說明屬性值所佔用的位數長度即可。一個符合規則的屬性表至少應具有“attribute_name_info”、“attribute_length”和至少一項資訊屬性。

 

文章內容來源於《深入理解Java虛擬機器》

部分內容參考了下面的部落格
原文:https://blog.csdn.net/ns_code/article/details/17675609