1. 程式人生 > >第六章 類檔案結構

第六章 類檔案結構

6.1 概述

      程式碼編譯的結果是從本地機器碼轉變為位元組碼,是儲存格式發展的一小步,卻是程式語言發展的一大步。由於最近10年內虛擬機器及建立在虛擬機器之上的大量程式設計語言如雨後春筍般出現並蓬勃發展,將我們的程式編譯成二進位制本地機器碼已不再是唯一的選擇,越來越多的程式語言選擇了與作業系統和機器指令集無關的、平臺中立的格式作為程式編譯後的儲存格式。

6.2 無關性的基石

      各種不同平臺的虛擬機器與所有平臺都統一使用的程式儲存格式——位元組碼是構成平臺無關性的基石。使用java編譯器可以吧java程式碼編譯為儲存位元組碼的class檔案,使用JRuby等其他語言的編譯器一樣可以把程式程式碼編譯成class檔案,虛擬機器並不關心class的來源是什麼語言,只要它符合class檔案應有的結構就可以在java虛擬機器中執行。

      java語言中的各種變數、關鍵字和運算子號的語義最終都是由多條位元組碼命令組合而成的,因此位元組碼命令所能提供的語義描述能力肯定會比java語言本身更強大。因此有一些java語言本身無法有效支援的語言特性並不代表位元組碼本身無法有效支援,這也為其他語言實現一些有別於java的語言特性提供了基礎。

6.3 Class類檔案的結構

      Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎全是程式執行的必要資料,沒有空隙存在。當遇到有8位位元組以上空間的資料項時,則會按照高位在前的方式 分割成若干個8位位元組進行儲存。

      根據java虛擬機器規範的規定,Class檔案格式採用一種類似於c語言結構體的偽結構來儲存,這種偽結構只有兩種資料型別:無符號數和表,後面的解析都要以這兩種資料型別為基礎。

      無符號數屬於基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組、8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字串。表是由多個無符號數或其他表作為資料項構成的複合資料型別,所有表都習慣性的以“info”結尾。表用於描述有層次關係的複合結構的資料,整個Class檔案本質上就是一張表。

6.3.1 魔數與Class檔案的版本

      每個Class檔案的頭4個位元組稱為魔數,它的唯一作用是用於確定這個檔案是否為一個能被虛擬機器接受的class檔案。使用魔數而不使用副檔名來進行識別主要是基於安全考慮,因為副檔名可以很隨意的被改動。

      緊接著魔數的4個位元組儲存的是Class檔案的版本號:第5和第6個位元組是次版本號,第7和第8個位元組是主版本號。java的版本號是從45開始的,JDK1.1之後的每個JDK大版本釋出主版本號向上加一,高版本的JDK可以相容以前版本的Class檔案,但不能執行以後版本的Class檔案,即使檔案格式並沒有發生改變。

6.3.2 常量池

      緊接著主次版本號之後的是長兩次常量池入口,常量池是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一,同時它還是在Class檔案中第一個出現的表型別資料專案。

      由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2型別的資料,代表常量池容量計數值。與java中的語言習慣不同,這個容量計數是從1而不是0開始的,常量池容量(偏移地址:0x00000008)為16進位制數0x0016,即十進數的22,這就代表常量池中有21項常量,索引值為1-21.制定Class檔案規範時,將第0項常量空出來是有特殊考慮的,這樣做是為了滿足後面某些指向常量池的索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的意思,這種情況就可以把索引值置為0來表示。Class檔案結構中只有常量池的容量計數是從1開始的,對於其他集合型別,包括介面索引集合、欄位表集合、方法表集合等的容量計數都與一般習慣相同,是從0開始的。

      常量池之中主要存放兩大類常量:字面量和符號引用。字面量比較接近於java語言層面的常量概念,如文字字串、被宣告為final的常量值等。而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:類和介面的全限定名,欄位的名稱和描述符,方法的名稱和描述符。

6.3.3 訪問標誌

      在常量池結束之後,緊接著的兩個位元組代表訪問標誌(access flags),這個標誌用於識別一些類或介面層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public型別;是否定義為abstract型別;如果是類的話,是否被宣告為final等等。

6.3.4 類索引、父類索引與介面索引集合

      類索引和父類索引都是一個u2型別的資料,而介面索引集合是一組u2型別資料的集合,Class檔案中由這三項資料來確定這個類的繼承關係。類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。由於java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的java類都有父類,因此除了java.lang.Object之外,所有java類的父類索引都不為0。介面索引集合就用來描述這個類實現了哪些介面,這些被實現的介面將按implements語句後的介面順序從左到右排列在介面的索引集合中。

6.3.5 欄位表集合

      欄位表用於描述介面或類中宣告的變數。欄位包括了類級變數或例項級變數,但不包括在方法內部宣告的變數。

6.3.6 方法表集合

      方法表的結構如欄位表一樣,依次包括了訪問標誌、名稱索引、描述符索引、屬性表集合幾項。方法的定義可以通過訪問標誌、名稱索引、描述符索引表達清楚方法裡的程式碼去哪裡了?方法裡的java程式碼經過編譯器編譯成位元組碼之後,存放在方法屬性表集合中一個名為“code”屬性裡面,屬性表作為Class檔案格式中最具擴充套件性的一種資料專案。

6.3.7 屬性表集合

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

a、code屬性:java程式方法體裡面的程式碼經過javac編譯器處理之後,最終變為位元組碼指令儲存在Code屬性內。Code屬性出現在方法表的屬性集合之中,但並非所有的方法表都必須存在這個屬性,譬如介面或抽象類中的方法就不存在Code屬性。

b、Exceptions屬性:作用是列舉出方法中可能丟擲的受查異常,也就是方法描述時在throws關鍵字後面列舉的異常。

c、LineNumberTable屬性:用於描述java原始碼行號與位元組碼行號之間的對應關係。

此外還有SourceFile屬性、ConstantValue屬性、InnerClasses屬性、Deprecated及Synthetic屬性等等。