[深入理解Java虛擬機器]第六章 Class類檔案的結構

Coding-lover發表於2015-10-22

在本章關於Class檔案結構的講解中,我們將以《Java虛擬機器規範(第2版 )》 (1999年釋出,對應於JDK 1.4時代的Java虛擬機器)中的定義為主線,這部分內容雖然古老,但它所包含的指令、屬性是Class檔案中最重要和最基礎的。同時,我們也會以後續JDK 1.5〜 JDK 1.7中新增的內容為支線進行較為簡略的、介紹性的講解,如果讀者對這部分內容特別感興趣, 建議參考筆者所翻譯的《Java虛擬機器規範( Java SE 7 ) 》中文版,可以在筆者的網站(http://icyfenix.iteye.com/)上下載到這本豐的全文PDF。

注意任何一個Class檔案都對應著唯一一個類或介面的定義資訊,但反過來說,類或介面並不一定都得定義在檔案裡(譬如類或介面也可以通過類載入器直接生成)。 本章中 ,筆者只是通俗地將任意一個有效的類或介面所應當滿足的格式稱為“Class檔案格式”,實際上它並不一定以磁碟檔案的形式存在。

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

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

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

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

無論是無符號數還是表,當需要描述同一型別但數量不定的多個資料時,經常會使用一個前置的容量計數器加若干個連續的資料項的形式,這時稱這一系列連續的某一型別的資料為某一型別的集合。

本節結束之前,筆者需要再重複講一下,Class的結構不像XML等描述語言,由於它沒有任何分隔符號,所以在表6-1中的資料項,無論是順序還是數量,甚至於資料儲存的位元組序( Byte Ordering,Class檔案中位元組序為Big-Endian)這樣的細節,都是被嚴格限定的,哪個位元組代表什麼含義,長度是多少,先後順序如何,都不允許改變。接下來我們將一起看看這個表中各個資料項的具體含義。

魔數與Class檔案的版本

每個Class檔案的頭4個位元組稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。很多檔案儲存標準中都使用魔數來進行身份識別, 譬如圖片格式,如運域者jpeg等在檔案頭中都存有魔數。使用魔數而不是副檔名來進行識別主要是基於安全方面的考慮,因為副檔名可以隨意地改動。檔案格式的制定者可以自由地選擇魔數值,只要這個魔數值還沒有被廣泛採用過同時又不會引起混淆即可。Class檔案的魔數的獲得很有“浪漫氣息” ,值為: OxCAFEBABE ( 咖啡寶貝?),這個魔數值在Java還稱做“Oak”語言的時候(大約是1991年前後)就已經確定下來了。它還有一段很有趣的歷史, 據Java開發小組最初的關鍵成員Patrick Naughton所說 :“我們一直在尋找一些好玩的、容易記憶的東西,選擇OxCAFEBABE是因為它象徵著著名咖啡品牌Peet’s Coffee中深受歡迎的Baristas咖啡”,這個魔數似乎也預示著日後“Java”這個商標名稱的出現。

緊接著魔數的4個位元組儲存的是Class檔案的版本號:第5和第6個位元組是次版本號( Minor Version) ,第7和第8個位元組是主版本號(Major Version)。 Java的版本號是從45開始的 , JDK 1.1之後的每個JDK大版本釋出主版本號向上加1(JDK 1.0〜1.1使用了45.0〜45.3的版本號),高版本的JDK能向下相容以前版本的Class檔案,但不能執行以後版本的Class檔案 ,即使檔案格式並未發生任何變化,虛擬機器也必須拒絕執行超過其版本號的Class檔案。

例如, JDK 1.1能支援版本號為45.0〜45.65535的Class檔案,無法執行版本號為46.0以上的Class文 件 ,而JDK 1.2則能支援45 .0〜46.65535的Class檔案。現 在 ,最新的JDK版本為 1.7,可生成的Class檔案主版本號最大值為51.0。

為了講解方便,筆者準備了一段最簡單的Java程式碼(見程式碼清單6-1 ) ,本章後面的內容都將以這段小程式使用JDK 1.6編譯輸出的Class檔案為基礎來進行講解。

package org.fenixsoft.clazz;

public class TestClass {

    private int m;

    public int inc() {
        return m + 1;
    }
}

圖6-2顯示的是使用十六進位制編輯器WinHex開啟這個Class檔案的結果,可以清楚地看見開頭4個位元組的十六進位制表示是OxCAFEBABE,代表次版本號的第5個和第6個位元組值為 0x0000 ,而主版本號的值為0x0032 ,也即是十進位制的50,該版本號說明這個檔案是可以被 JDK 1.6或以上版本虛擬機器執行的Class檔案。

表6-2列出了從JDK 1.1到JDK 1.7 , 主流JDK版本編譯器輸出的預設和可支援的Class檔案版本號。

這種順序稱為“Big-Endian” ,具體是指最高位位元組在地址最低位、最低位位元組在地址最高位的順序來儲存資料,它是SPARC、PowerPC等處理器的預設多位元組儲存順序,而x86等處理器則是使用了相反的“Little-Endian”順序來儲存資料。

常量池

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

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

常量池中主要存放兩大類常量:字面量( Literal ) 和符號引用( Symbolic References )。 字面量比較接近於Java語言層面的常量概念,如文字字串、宣告為final的常量值等。而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:

  • 類和介面的全限定名( Fully Qualified Name )
  • 欄位的名稱和描述符( Descriptor )
  • 方法的名稱和描述符

Java程式碼在進行Javac編譯的時候,並不像C和C++那樣有“連線”這一步驟,而是在虛擬機器載入Class檔案的時候進行動態連線。也就是說,在Class檔案中不會儲存各個方法、欄位的最終記憶體佈局資訊,因此這些欄位、方法的符號引用不經過執行期轉換的話無法得到真正的記憶體入口地址,也就無法直接被虛擬機器使用。當虛擬機器執行時,需要從常量池獲得對應的符號引用,再在類建立時或執行時解析、翻譯到具體的記憶體地址之中。

常量池中每一項常量都是一個表,在JDK 1.7之前共有11種結構各不相同的表結構資料 ,在JDK 1.7中為了更好地支援動態語言呼叫,又額外增加了3種
( CONSTANT_MethodHandle_info、 CONSTANT_MethodType_info和 CONSTANT_InvokeDynamic_info,本章不會涉及種新增的型別 ,在第8章介紹位元組碼執行和方法呼叫時,將會詳細講解)。

這14種表都有一個共同的特點,就是表開始的第一位是一個u1型別的標誌位(tag,取值見表6-3中標誌列),代表當前這個常量屬於哪種常量型別。這14種常量型別所代表的具體 含義見表6-3。

之所以說常量池是最煩瑣的資料,是因為這14種常量型別各自均有自己的結構。回頭看看圖6-3中常量池的第一項常量,它的標誌位(偏移地址: 0x0000000A ) 是0x07,查表6-3的標誌列發現這個常量屬於CONSTANT_Class_info類 型 ,此型別的常量代表一個類或者介面的符號引用。CONSTANT_Class_info的結構比較簡單,見表6-4。

tag是標誌位,上面已經講過了,它用於區分常量型別;name_index是一個索引值,它指向常量池中一個CONSTANT_Utf8_info型別常量,此常量代表了這個類(或者介面)的全限定名,這裡name_index值(偏移地址:0x0000000B)為0x0002 ,也即是指向了常量池中的第二項常量。繼續從圖6-3中查詢第二項常量,它的標誌位(地址:0x0000000D)是0x01 ,查表6-3可知確實是一個CONSTANT_Utf8_info型別的常量。CONSTANT_Utf8_info型別的結構見表6-5:

length值說明了這個UTF-8編碼的字串長度是多少位元組,它後面緊跟著的長度為length位元組的連續資料是一個使用UTF-8縮略編碼表示的字串。UTF-8縮略編碼與普通UTF-8編碼 的區別是:從‘\u0001’到’\u007f之間的字元(相當於1 〜127的ASCII碼 )的縮略編碼使用一個位元組表示,從‘\u0080’到‘\u07ff’之間的所有字元的縮略編碼用兩個位元組表示,從’\u0800’到’\uffff’之間的所有字元的縮略編碼就按照普通UTF-8編碼規則使用三個位元組表示。

順便提一下,由於Class檔案中方法、欄位等都需要引用CONSTANT_Utf8_info型常量來描述名稱,所以CONSTANT_Utf8_info型常量的最大長度也就是Java中方法、欄位名的最大長度。而這裡的最大長度就是length的最大值,既u2型別能表達的最大值65535。所以Java程式中如果定義了超過64KB英文字元的變數或方法名,將會無法編譯。

本例中這個字串的length值(偏移地址:0x0000000E)為0x001D, 也就是長29位元組, 往後29位元組正好命在1〜127的ASCII碼範圍以內,內容為“org/fenixsofl/clazz/TestClass”,有興趣的讀者可以自己逐個位元組換算一下,換算結果如圖6-4選中的部分所示。

到此為止,我們分析了TestClass.class常量池中21個常量中的兩個,其餘的19個常量都可以通過類似的方法計算出來。為了避免計算過程佔用過多的版面,後續的19個常量的計算過程可以藉助計算機來幫我們完成。在JDK的bin目錄中,Oracle公司已經為我們準備好一個專門用於分析Class檔案位元組碼的工具:javap ,程式碼清單6-2中列出了使用javap工具的-verbose 引數輸出的TestClass.class檔案位元組碼內容(此清單中省略了常量池以外的資訊)。前面我們曾經提到過,Class檔案中還有很多資料項都要引用常量池中的常量,所以程式碼清單6-2中的內容在後續的講解過程中還要經常使用到。

從程式碼清單6-2中可以看出,計算機已經幫我們把整個常量池的21項常量都計算了出來 ,並且第1、2項常量的計算結果與我們手工計算的結果一致。仔細看一下會發現,其中有—些常量似乎從來沒有在程式碼中出現過,如“I”、“V”、“ <init>”、“LineNumberTable”、 “LocalVariableTable”等 ,這些看起來在程式碼任何一處都沒有出現過的常量是哪裡來的呢?

這部分自動生成的常量的確沒有在Java程式碼裡面直接出現過,但它們會被後面即將講到的欄位表( field_info ) 、方法表(method_info ) 、屬性表(attribute_info ) 引用到,它們會用來描述一些不方便使用“固定位元組”進行表達的內容。譬如描述方法的返回值是什麼?有幾個引數?每個引數的型別是什麼?因為Java中的“類”是無窮無盡的,無法通過簡單的無符號位元組來描述一個方法用到了什麼類 ,因此在描述方法的這些資訊時,需要引用常量表中的符號引用進行表達。這部分內容將在後面進一步闡述。最後 ,筆者將這14種常量項的結構定義總
結為表6-6以供讀者參考。


訪問標誌

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

access_flags中一共有16個標誌位可以使用,當前只定義了其中8個’沒有使用到的標誌位要求為0。以程式碼清單6-1中的程式碼為例,TestClass是一個普通Java類 ,不是介面、列舉或者註解,被public關鍵字修飾但沒有被宣告為final和abstract, 並且它使用了JDK 1.2之後的編譯器進行編譯,因此它的ACC_PUBLIC、ACC_SUPER標誌應當為真,而ACC_FINAL、 ACC_INTERFACE、ACC_ABSTRACT、 ACC_SYNTHETIC、 ACC_ANNOTATION、 ACC_ENUM這6個標誌應S 為假,因此它的access_flags的值為:0x000110x0020=0x0021。 從圖;5中可以看出,access_flags標誌(偏移地址:0x000000EF ) 的確為0x0021。

在Java虛擬機器規範中,只定義了開頭5種標誌。了DK 1.5中增加了後面3種。這些標誌為在JSR-202規範中宣告,是對 《Java虛擬機器規範(第2版 )》的補充。本書介紹的訪問標誌以 JSR-202規範為準。

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

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

類索引、父類索引和介面索引集合都按順序排列在訪問標誌之後,類索引和父類索引用兩個u2型別的索引值表示,它們各自指向一個型別為CONSTANT_Class_info的類描述符常量 ,通過CONSTANT_Class_info型別的常量中的索引值可以找到S 義在 C0NSTANT_Utf8_mfo型別的常量中的全限定名字串。圖6-6演示了程式碼清單6-1的程式碼的類索引查詢過程 。

對於介面索引集合,入口的第一項——u2型別的資料為介面計數器
( interfaces_count), 表示索引表的容量。如果該類沒有實現任何介面,則該計數器值為0 ,後面介面的索引表不再佔用任何位元組。程式碼清單6-1中的程式碼的類索引、父類索引與介面表 索引的內容如圖6-7所示。

從偏移地址0x000000F1開始的3個U2型別的值分別為0x0001、0x0003、0x0000, 也就是類索引為1 ,父類索引為3 ,介面索引集合大小為0 ,查詢前面程式碼清單6-2中javap命令計算出來的常量池,找出對應的類和父類的常量,結果如程式碼清單6-3所示。

欄位表集合

欄位表( field_info ) 用於描述介面或者類中宣告的變數。欄位( field )包括類級變數以及例項級變數,但不包括在方法內部宣告的區域性變數。我們可以想一想在Java中描述一個欄位可以包含什麼資訊?可以包括的資訊有:欄位的作用域(public、private、protected修飾符)、是例項變數還是類變數(static修飾符)、可變性( final ) 、併發可見性( volatile修飾符 ,是否強制從主記憶體讀寫)、可否被序列化(transient修飾符)、欄位資料型別(基本型別、物件、陣列 )、欄位名稱。上述這些資訊中,各個修飾符都是布林值,要麼有某個修飾符 ,要麼沒有,很適合使用標誌位來表示。而欄位叫什麼名字、欄位被定義為什麼資料型別 ,這些都是無法固定的,只能引用常量池中的常量來描述。表6-8中列出了欄位表的最終格式。

欄位修飾符放在access_flags專案中,它與類中的access_flags專案是非常類似的,都是一 個u2的資料型別,其中可以設定的標誌位和含義見表6-9。

很明顯,在實際情況中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三個標誌最多隻能選擇其一 , ACC_FINAL、ACC_VOLATILE不能同時選擇。介面之中的欄位必須有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL標誌 ,這些都是由Java本身的語言規則所決定的。

跟隨access_flags標誌的是兩項索引值:name_index和descriptor_index。它們都是對常量池的引用,分別代表著欄位的簡單名稱以及欄位和方法的描述符現在需要解釋一下“簡單名稱” 、“ 描述符” 以及前面出現過多次的“ 全限定名” 這三種特殊字串的概念。

全限定名和簡單名稱很好理解,以程式碼清單6-1中的程式碼為
例 ,“org/fenixsoft/clazz/TestClass”是這個類的全限定名,僅僅是把類全名中的“.”替換成了“/”而已,為了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個“;”表示全限定名結束。簡單名稱是指沒有型別和引數修飾的方法或者欄位名稱,這個類中的inc() 方法和m欄位的簡單名稱分別是“inc”和“m”。

相對於全限定名和簡單名稱來說,方法和欄位的描述符就要複雜一些。描述符的作用是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序)和返回值。根據描述符規則,基本資料型別(byte、char、double、float、int、long、short、boolean)以及代表無返回值的void型別都用一個大寫字元來表示,而物件型別則用字元L加物件的全限定名來表示 ,詳見表6-10。

對於陣列型別,每一維度將使用一個前置的“[”字元來描述,如一個定義 為“java.lang.String[][]”型別的二維陣列,將被記錄為:“[[Ljava/lang/String ;”,一個整型陣列將被記錄為“ [I ”。

用描述符來描述方法時,按照先引數列表,後返回值的順序描述,引數列表按照引數的嚴格順序放在一組小括號“ ()”之內。如方法void inc ( ) 的描述符為“ ()V”方法 java.lang.String toString() 的描述符為“ ()Ljava/lang/String;” ,方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCountintfromlndex)的描述符為“ ([CII[CIII) I”。

對於程式碼清單6-1中的TestClass.class檔案來說,欄位表集合從地址0x000000F8開始,第1個u2型別的資料為容量計數器fields_count,如圖6-8所示,其值為0x0001 ,說明這個類只有一個欄位表資料。接下來緊跟著容量計數器的是access_flags標誌,值為0x0002,代表private修飾符的ACC_PRIVATE標誌位為真(ACC_PRIVATE標誌的值為0x0002 ) ,其他修飾符為假。代表欄位名稱的name index的值為0x0005,從程式碼清單6-2列出的常量中可查得第5項常量是一個CONSTANT_Utf8_info型別的字串,其值為“m ” ,代表欄位描述符的descriptor_index的值為0x0006,指向常量池的字串“I” ,根據這些資訊,我們可以推斷出原始碼定義的欄位為: “private int m;”。

欄位表都包含的固定資料專案到descriptor_index為止就結束了,不過在descriptor_index之後跟隨著一個屬性表集合用於儲存一些額外的資訊,欄位都可以在屬性表中描述零一至多項的額外資訊。對於本例中的欄位m ,它的屬性表計數器為0 ,也就是沒有需要額外描述的資訊 ,但是 ,如果將欄位m的宣告改為“final static int m=123 ; ”,那就可能會存在一項名稱為ConstantValue屬性,其值指向常量123。關於attribute_info的其他內容,將在6.3.7節介紹屬性表的資料專案時再進一步講解。

欄位表集合中不會列出從超類或者父介面中繼承而來的欄位,但有可能列出原本Java程式碼之中不存在的欄位,譬如在內部類中為了保持對外部類的訪問性,會自動新增指向外部類例項的欄位。另外 ,在Java語言中欄位是無法過載的,兩個欄位的資料型別、修飾符不管是否相同,都必須使用不一樣的名稱,但是對於位元組碼來講,如果兩個欄位的描述符不一致, 那欄位重名就是合法的。

void型別在虛擬機器規範之中單獨列出為“VoidDescnptor” ,筆者為了結構統一,將其列在基本資料型別中一起描述。

方法表集合

如果理解了上一節關於欄位表的內容,那本節關於方法表的內容將會變得很簡單。 Class檔案儲存格式中對方法的描述與對欄位的描述幾乎採用了完全一致的方式,方法表的結構如同欄位表一樣,依次包括了訪問標誌(access_flags ) 、名稱索引(name_index)、描述符索引 ( descriptor_index ) 、屬性表集合( attributes ) 幾項,見表6-11。這些資料專案的含義也非常類似 ,僅在訪問標誌和屬性表集合的可選項中有所區別。

因為volatile關鍵字和transient關鍵字不能修飾方法,所以方法表的訪問標誌中沒有了ACC_VOLATILE標誌和ACC_TRANSIENT標誌。與之相對的,synchronized、native、 strictfp 和abstract關鍵字可以修飾方法,所以方法表的訪問標誌中增加了ACC_SYNCHRONIZED、 ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT標誌。對於方法表,所有標誌位及其取值可參見表6-12。

行文至此,也許有的讀者會產生疑問,方法的定義可以通過訪問標誌、名稱索引、描述符索引表達清楚,但方法裡面的程式碼去哪裡了?方法裡的Java程式碼 ,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性裡面,屬性表作為Class檔案格式中最具擴充套件性的一種資料專案,將在6.3.7節中詳細講解。

我們繼續以程式碼清單6-1中的Class檔案為例對方法表集合進行分析,如圖6-9所示,方法表集合的入口地址為:0x00000101, 第一個u2型別的資料(即是計數器容量)的值為 0x0002 , 代表集合中有兩個方法(這兩個方法為編譯器新增的例項構造器<init>和原始碼中的方法inc())。第一個方法的訪問標誌值為0x001 ,也就是隻有ACC_PUBLIC標誌為真, 名稱索引值為0x0007,查程式碼清單6-2的常量池得方 法名為“<init>” ,描述符索引值為 0x0008, 對應常量為“()V ’ , 屬性表計數器attributes_count的值為0x0001就表示此方法的屬性表集合有一項屬性,屬性名稱索引為0x0009,對應常量為“Code” ,說明此屬性是方法的位元組碼描述。

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

在Java語言中,要過載(Overload ) 一個方法,除了要與原方法具有相同的簡單名稱之外 ,還要求必須擁有一個與原方法不同的特徵簽名 ,特徵簽名就是一個方法中各個引數在常量池中的欄位符號引用的集合,也就是因為返回值不會包含在特徵簽名中,因此Java語言裡面是無法僅僅依靠返回值的不同來對一個已有方法進行過載的。但是在Class檔案格式中,特徵簽名的範圍更大一些,只要描述符不是完全一致的兩個方法也可以共存。也就是說,如果兩個方法有相同的名稱和特徵簽名,但返回值不同,那麼也是可以合法共存於同一個Class檔案中的。

在《Java虛擬機器規範(第2版)》的“§4.4.4 Signatures”章節及《Java語言規範(第3版)》 的“§8.4.2 Method Signature”章節中都分別定義了位元組碼層面的方法特徵簽名以及Java程式碼層面的方法特徵簽名,Java程式碼的方法特徵簽名只包括了方法名稱、引數順序及引數型別,而位元組碼的特徵簽名還包括方法返回值以及受查異常表,請讀者根據上下文語境注意區分。

屬性表集合

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

與Class檔案中其他的資料專案要求嚴格的順序、長度和內容不同,屬性表集合的限制稍微寬鬆了一些,不再要求各個屬性表具有嚴格順序,並且只要不與已有屬性名重複,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java虛擬機器執行時會忽略掉它不認識的屬性。為了能正確解析Class文 件 ,《Java虛擬機器規範(第2版 )》中預定義了9項虛擬機器實現應當能識別的屬性,而在最新的《Java虛擬機器規範( Java SE 7 ) 》版中,預定義屬性已經增加到21項 ,具體內容見表6-13。下文中將對其中一些屬性中的關鍵常用的部分進行講解。



對於每個屬性,它的名稱需要從常量池中引用一個CONSTANT_Utf8_info型別的常量來表示,而屬性值的結構則是完全自定義的,只需要通過一個u4的長度屬性去說明屬性值所佔用的位數即可。一個符合規則的屬性表應該滿足表6-14中所定義的結構。

1.Code屬性

Java程式方法體中的程式碼經過Javac編譯器處理後,最終變為位元組碼指令儲存在Code屬性內。Code屬性出現在方法表的屬性集合之中,但並非所有的方法表都必須存在這個屬性,譬如介面或者抽象類中的方法就不存在Code屬性 ,如果方法表有Code屬性存在,那麼它的結構將如表6-15所示。

attribute_name_index是一項指向CONSTANT_Utf8_info型常量的索引,常量值固定為“Code” ,它代表了該屬性的屬性名稱, attribute_length指示了屬性值的長度 ,由於屬性名稱索引與屬性長度一共為6位元組,所以屬性值的長度固定為整個屬性表長度減去6個位元組。

max_stack代表了運算元棧( Operand Stacks ) 深度的最大值。在方法執行的任意時刻, 運算元棧都不會超過這個深度。虛擬機器執行的時候需要根據這個值來分配棧幀( Stack Frame ) 中的操作棧深度。

max_locals代表了區域性變數表所需的儲存空間。在這裡, max_locals的單位是Slot,Slot是虛擬機器為區域性變數分配記憶體所使用的最小單位。對於byte、char、float、int、short、 boolean 和retumAddress等長度不超過32位的資料型別,每個區域性變數佔用1個Slot,而double和long這 兩種64位的資料型別則需要兩個Slot來存放。方法引數(包括例項方法中的隱藏引數“this” )、顯式異常處理器的引數( Exception Handler Parameter,就是try-catch語句中catch塊所定義的異常)、方法體中定義的區域性變數都需要使用區域性變數表來存放。另外 ,並不是 在方法中用到了參少個區域性變數,就把這些區域性變數所佔Slot之和作為max_locals的i值,原因是區域性變數表中的Slot可以重用,當程式碼執行超出一個區域性變數的作用域時,這個區域性變數所佔的Slot可以被其他區域性變數所使用,Javac編譯器會根據變數的作用域來分配Slot給各個變數使用,然後計算出max_locals的大小。

code_length和code用來儲存Java源程式編譯後生成的位元組碼指令。 code length代表位元組碼長度 ,code是用於儲存位元組碼指令的一系列位元組流。既然叫位元組碼指令,那麼每個指令就是 一個u1型別的單位元組,當虛擬機器讀取到code中的一個位元組碼時,就可以對應找出這個位元組碼代表的是什麼指令,並且可以知道這條指令後面是否需要跟隨引數,以及引數應當如何理解。我們知道一個u1資料型別的取值範圍為0x00〜OxFF,對應十進位制的0〜255,也就是一共可以表達256條 指令 ,目前 ,Java虛擬機器規範已經定義了其中約200條編碼值對應的指令含義 ,編碼與指令之間的對應關係可查閱本書的附錄B“虛擬機器位元組碼指令表”。

關於code_length,有一件值得注意的事情,雖然它是一個說型別的長度值,理論上最大值可以達到232-1 ,但是虛擬機器規範中明確限制了 一個方法不允許超過65535條位元組碼指令, 即它實際只使用了u2的長度,如果超過這個限制,Javac編譯器也會拒絕編譯。一般來講,編寫Java程式碼時只要不是刻意去編寫一個超長的方法來為難編譯器,是不太可能超過這個最大值的限制。但是,某些特殊情況,例如在編譯一個很複雜的JSP檔案時,某些JSP編譯器會把JSP內容和頁面輸出的資訊歸併於一個方法之中,就可能因為方法生成位元組碼超長的原因而導致編譯失敗。

Code屬性是Class檔案中最重要的一個屬性,如果把一個Java程式中的資訊分為程式碼( Code,方法體裡面的Java程式碼)和後設資料(Metadata,包括類、欄位、方法定義及其他資訊 )兩部分,那麼在整個Class檔案中,Code屬性用於描述程式碼,所有的其他資料專案都用於描述後設資料。瞭解Code屬性是學習後面關於位元組碼執行引擎內容的必要基礎,能直接閱讀位元組碼也是工作中分析Java程式碼語義問題的必要工具和基本技能,因此筆者準備了一個比較詳細的例項來講解虛擬機器是如何使用這個屬性的。

繼續以程式碼清單6-1的TestClass.class檔案為例,如圖6-10所示,這是上一節分析過的例項構造器“ <init> ”方法的Code屬性。它的運算元棧的最大深度和本地變數表的容量都為 0x0001,位元組碼區域所佔空間的長度為0x0005。虛擬機器讀取到位元組碼區域的長度後,按照順序依次讀入緊隨的5個位元組 ,並根據位元組碼指令表翻譯出所對應的位元組碼指令。翻譯“2A B7 00 0A B1”的過程為:

  • 1 ) 讀入2A , 查表得0x2A對應的指令為aload_0 , 這個指令的含義是將第0個Slot中為reference型別的本地變數推送到運算元棧頂。
  • 2 ) 讀入B7 , 查表得0xB7對應的指令為invokespecial ,這條指令的作用是以棧頂的reference型別的資料所指向的物件作為方法接收者,呼叫此物件的例項構造器方法、 private方法或者它的父類的方法。這個方法有一個u2型別的引數說明具體呼叫哪一個方法,它指向常量池中的一個CONSTANT_Methodref_info型別常量,即此方法的方法符號引用。
  • 3 )讀入00 0A ,這是invokespecial的引數,查常量池得0x000A對應的常量為例項構造 器“<init> ”方法的符號引用。
  • 4 ) 讀入B1,查表得0xB1對應的指令為return,含義是返回此方法,並且返回值為void。 這條指令執行後,當前方法結束。

這段位元組碼雖然很短,但是至少可以看出它的執行過程中的資料交換、方法呼叫等操作都是基於棧(操作棧)的。我們可以初步猜測:Java虛擬機器執行位元組碼是基於棧的體系結構。但是與一般基於堆疊的零位元組指令又不太一樣,某些指令(如invokespecial)後面還會帶有引數,關於虛擬機器位元組碼執行的講解是後面兩章的重點,我們不妨把這裡的疑問放到第8章去解決。

我們再次使用javap命令把此Class檔案中的另外一個方法的位元組碼指令也計算出來,結果如程式碼清單6-4所示。


如果大家注意到javap中輸出的“Args_size”的值,可能會有疑問:這個類有兩個方法——例項構造器<init>()和inc(), 這兩個方法很明顯都是沒有引數的,為什麼Args_size會為1? 而且無論是在引數列表裡還是方法體內,都沒有定義任何區域性變數,那Locals又為什麼會等於1 ? 如果有這樣的疑問,大家可能是忽略了一點:在任何例項方法裡面,都可以通過“this”關鍵字訪問到此方法所屬的物件。這個訪問機制對Java程式的編寫很重要,而它的實現卻非常簡單,僅僅是通過Javac編譯器編譯的時候把對this關鍵字的訪問轉變為對一個普通方法引數的訪問,然後在虛擬機器呼叫例項方法時自動傳入此引數而已。因此在例項方法的區域性變數表中至少會存在一個指向當前物件例項的區域性變數,區域性變數表中也會預留出第一個 Slot位來存放物件例項的引用,方法引數值從1開始計算。這個處理只對例項方法有效,如果程式碼清單6-1中的inc()方法宣告為static, 那Args_size就不會等於1而是等於0了。

在位元組碼指令之後的是這個方法的顯式異常處理表(下文簡稱異常表)集合 ,異常表對於Code屬性來說並不是必須存在的,如程式碼清單6-4中就沒有異常表生成。

異常表的格式如表6-16所示 ,它包含4個欄位 ,這些欄位的含義為:如果當位元組碼在第 start_pc行到第end_pc行之間(不含第end_pc行)出現了型別為catch_type或者其子類的異常 ( catch_type為指向一個CONSTANT_Class_info型常量的索引),則轉到第handler_pc行繼續處理。當catch_type的值為0時 ,代表”任意異常情況都需要轉向到handler_pc處進行處理。

異常表實際上是Java程式碼的一部分,編譯器使用異常表而不是簡單的跳轉命令來實現Java異常及finally處理機制。

程式碼清單6-5是一段演示異常表如何運作的例子,這段程式碼主要演示了在位元組碼層面中try-catch-finally是如何實現的。在閱讀位元組碼之前,夫家不妨先看看下面的Java原始碼 ,想一下這段程式碼的返回值在出現異常和不出現異常的情況下分別應該是多少?


編譯器為這段Java原始碼生成了3條異常表記錄,對應3條可能出現的程式碼執行路徑。從Java程式碼的語義上講,這3條執行路徑分別為:

  • 如果try語句塊中出現屬於Exception或其子類的異常,則轉到catch語句塊處理。
  • 如果try語句塊中出現不屬於Exception或其子類的異常,則轉到finally語句塊處理。
  • 如果catch語句塊中出現任何異常,則轉到finally語句塊處理。

返回到我們上面提出的問題,這段程式碼的返回值應該是多少?對Java語言熟悉的讀者應該很容易說出答案:如果沒有出現異常,返回值是1 ; 如果出現了Exception異常,返回值是2 ;如果出現了Exception以外的異常,方法非正常退出,沒有返回值。我們一起來分析一下位元組碼的執行過程,從位元組碼的層面上看看為何會有這樣的返回結果。

位元組碼中第0〜4行所做的操作就是將整數1賦值給變數x ,並且將此時x的值複製一份副本到最後一個本地變數表的Slot中(這個Slot裡面的值在ireturn指令執行前將會被重新讀到操作棧頂,作為方法返回值使用。為了講解方便,筆者給這個Slot起了個名字: returnValue )。 如果這時沒有出現異常,則會繼續走到第5〜9行 ,將變數x賦值為3 ,然後將之前儲存在returnValue中的整數1讀入到操作棧頂,最後ireturn指令會以int形式返回操作棧頂中的值,方法結束。如果出現了異常,PC暫存器指標轉到第10行 ,第10〜20行所做的事情是將2賦值給變數x ,然後將變數x此時的值賦給returnValue ,最後再將變數x的值改為3。方法返回前同樣 將returnValue中保留的整數2讀到了操作棧頂。從第21行開始的程式碼,作用是變數x的值賦為3 ,並將棧頂的異常丟擲,方法結束。

儘管大家都知道這段程式碼出現異常的概率非常小,但並不影響它為我們演示異常表的作用。如果大家到這裡仍然對位元組碼的運作過程比較模糊,其實也不要緊,關於虛擬機器執行位元組碼的過程,本書第8章中將會有更詳細的講解。

2.Exceptions屬性

這裡的Exceptions屬性是在方法表中與Code屬性平級的一項屬性,讀者不要與前面剛剛講解完的異常表產生混淆。Exceptions屬性的作用是列舉出方法中可能丟擲的受查異常( Checked Excepitons ) ,也就是方法描述時在throws關鍵字後面列舉的異常。它的結構見表 6-17。

Exceptions屬性中的number_of_exceptions項表示方法可能丟擲number_of_exceptions種受查異常 ,每一種受查異常使用一個exception_index_table項表示 ,exception_index_table是一個指向常量池中CONSTANT_Class_info型常量的索引,代表了該受查異常的型別。

3.LineNumberTable屬性

LineNumberTable屬性用於描述Java原始碼行號與位元組碼行號(位元組碼的偏移量)之間的對應關係。它並不是執行時必需的屬性,但預設會生成到Class檔案之中,可以在Javac中分別使用-g:none或-g:lines選項來取消或要求生成這項資訊。如果選擇不生成LineNumberTable 屬性 ,對程式執行產生的最主要的影響就是當丟擲異常時,堆疊中將不會顯示出錯的行號, 並且在除錯程式的時候,也無法按照原始碼行來設定斷點。LineNumberTable屬性的結構見表6- 18。

line_nimber_table是一個數量為line_number_table_length、型別為line_number_info的集 合 , line_number_info表包括了start_pc和line_number兩個u2型別的資料項,前者是位元組碼行號 ,後者是Java原始碼行號。

4.LocalVariableTable屬性

LocalVariableTable屬性用於描述棧幀中區域性變數表中的變數與Java原始碼中定義的變數之間的關係,它也不是執行時必需的屬性,但預設會生成到Class檔案之中,可以在Javac中分別使用-g:none或-g:vars選項來取消或要求生成這項資訊。如果沒有生成這項屬性,最大的影響就是當其他人引用這個方法時,所有的引數名稱都將會丟失,IDE將會使用諸如arg0、 arg1之類的佔位符代替原有的引數名,這對程式執行沒有影響,但是會對程式碼編寫帶來較大不便,而且在除錯期間無法根據引數名稱從上下文中獲得引數值。LocalVariableTable屬性的 結構見表6-19。

其中,local_variable_info專案代表了一個棧幀中與原始碼中區域性變數的關聯,結構見6-20:

start_pc和length屬性分別代表了這個區域性變數的生命週期開始的位元組碼偏移量及其作用範圍覆蓋的長度,兩者結合起來就是這個區域性變數在位元組碼之中的作用域範圍。

name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分別代表了區域性變數的名稱以及這個區域性變數的描述符。

index是這個區域性變數在棧幀區域性變數表中Slot的位置。當這個變數資料型別是64位型別時 (double和long ) , 它佔用的Slot為index和index+1兩個。

順便提一下,在JDK1.5引入泛型之後,LocalVariableTable屬性增加了一個“姐妹屬性” : LocalVariableTypeTable ,這個新增的屬性結構與LocalVariableTable非常相似,僅僅是把記錄的欄位描述符的descriptor_index替換成了欄位的特徵簽名(Signature),對於非泛型型別來說 ,描述符和特徵簽名能描述的資訊是基本一致的,但是泛型引入之後,由於描述符中泛型的引數化型別被擦除掉,描述符就不能準確地描述泛型型別了,因此出現了 LocalVariableTypeTable。

5.SourceFile屬性

SourceFile屬性用於記錄生成這個Class檔案的原始碼檔名稱。這個屬性也是可選的,可以分別使用Javac的-g:none或-g:source選項來關閉或要求生成這項資訊。在Java中 ,對於大多數的類來說,類名和檔名是一致的,但是有一些特殊情況(如內部類)例外。如果不生成這項屬性,當丟擲異常時,堆疊中將不會顯示出錯程式碼所屬的檔名。這個屬性是一個定長的屬性,其結構見表6-21。

sourcefile_index資料項是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是原始碼檔案的檔名。

6.ConstantValue屬性

ConstantValue屬性的作用是通知虛擬機器自動為靜態變數賦值。只有被static關鍵字修飾的變數(類變數)才可以使用這項屬性。類似“int x=123”和“static int x=123”這樣的變數定義在Java程式中是非常常見的事情,但虛擬機器對這兩種變數賦值的方式和時刻都有所不同。對於非static型別的變數(也就是例項變數)的賦值是在例項構造器<init>方法中進行的;而對於類變數,則有兩種方式可以選擇:在類構造器<clinit>方法中或者使用ConstantValue屬性。目前Sun Javac編譯器的選擇是:如果同時使用final和static來修飾一個變數(按照習慣, 這裡稱“常量”更貼切),並且這個變數的資料型別是基本型別或者java.lang.String的話 ,就生成ConstantValue屬性來進行初始化,如果這個變數沒有被final修飾 ,或者並非基本型別及字串 ,則將會選擇在<clinit>方法中進行初始化。

雖然有final關鍵字才更符合“ConstantValue” 的語義 ,但虛擬機器規範中並沒有強制要求欄位必須設定了ACC_FINAL標誌 ,只要求了有ConstantValue屬性的欄位必須設定ACC_STATIC 標誌而已,對final關鍵字的要求是Javac編譯器自己加入的限制。而對ConstantValue的屬性值只能限於基本型別和String,不過筆者不認為這是什麼限制,因為此屬性的屬性值只是一個常量池的索引號,由於Class檔案格式的常量型別中只有與基本屬性和字串相對應的字面量 ,所以就算ConstantValue屬性想支援別的型別也無能為力。ConstantValue屬性的結構見表 6- 22。

從資料結構中可以看出,ConstantValue屬性是一個定長屬性,它的attribute_length資料項值必須固定為2。constantvalue_index資料項代表了常量池中一個字面量常量的引用,根據欄位型別的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、 CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_Sring_info常量中的一種。

7.InnerClasses屬性

ImerClasses屬性用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那編譯器將會為它以及它所包含的內部類生成InnerClasses屬性。該屬性的結構見表6-23。

資料項number_of_classes表需要記錄多少個內部類資訊,每一個內部類的資訊都由一個inner_classes_info表進行描述。inner_classes_info表的結構見表6-24。

inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info 型常量的素引,分別代表了內部類和宿主類的符號引用。
imer_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表這個內部類的名稱,如果是匿名內部類,那麼這項值為0。
inner_class_access_flags是內部類的訪問標誌,類似於類的access_flags ,它的取值範圍見表6-25.

8.Deprecated及Synthetic屬性

Deprecated和Synthetic兩個屬性都屬於標誌型別的布林屬性,只存在有和沒有的區別,沒有屬性值的概念。

Deprecated屬性用於表示某個類、欄位或者方法,已經被程式作者定為不再推薦使用,它可以通過在程式碼中使用@deprecated註釋進行設定。

Synthetic屬性代表此欄位或者方法並不是由Java原始碼直接產生的,而是由編譯器自行新增的 ,在JDK 1.5之後 ,標識一個類、欄位或者方法是編譯器自動產生的,也可以設定它們訪問標誌中的ACC_SYNTHETIC標誌位,其中最典型的例子就是Bridge Method。所有由非使用者程式碼產生的類、方法及欄位都應當至少設定Synthetic屬性和ACC_SYNTHETIC標誌位中的一項,唯一的例外是例項構造器“<init>”方法和類構造器“<clinit>”方法。

Deprecated和Synthetic屬性的結構非常簡單,見表6-26。

其中attribute_length資料項的值必須為0x00000000,因為沒有任何屬性值需要設定。

9.StackMapTable屬性

StackMapTable屬性在JDK 1.6釋出後增加到了Class檔案規範中,它是一個複雜的變長屬性 ,位於Code屬性的屬性表中。這個屬性會在虛擬機器類載入的位元組碼驗證階段被新型別檢查驗證器( Type Checker )使用(見7.3.2節),目的在於代替以前比較消耗效能的基於資料流分析的型別推導驗證器。

這個型別檢查驗證器最初來源於Sheng Liang(聽名字似乎是虛擬機器團隊中的華裔成員) 為Java ME CLDC實現的位元組碼驗證器。新的驗證器在同樣能保證Class檔案合法性的前提下 ,省略了在執行期通過資料流分析去確認位元組碼的行為邏輯合法性的步驟,而是在編譯階段將一系列的驗證型別( Verification Types ) 直接記錄在Class檔案之中,通過檢查這些驗證型別代替了型別推導過程,從而大幅提升了位元組碼驗證的效能。這個驗證器在JDK 1.6中首次提供 ,並在JDK 1.7中強制代替原本基於型別推斷的位元組碼驗證器。關於這個驗證器的工作原理,《Java虛擬機器規範( Java SE 7版 )》花費了整整120頁的篇幅來講解描述,並且分析證明新驗證方法的嚴謹性,筆者在此不再贅述。

StackMapTable屬性中包含零至多個棧對映幀( Stack Map Frames ) ,每個棧對映幀都顯式或隱式地代表了一個位元組碼偏移量,用於表示該執行到該位元組碼時區域性變數表和運算元棧的驗證型別。型別檢查驗證器會通過檢查目標方法的區域性變數和運算元棧所需要的型別來確定一段位元組碼指令是否符合邏輯約束。StackMapTable屬性的結構見表6-27。

10. Signature屬性

Signature屬性在JDK 1.5釋出後增加到了Class檔案規範之中,它是一個可選的定長屬性, 可以出現於類、屬性表和方法表結構的屬性表中。在JDK 1.5中大幅增強了Java語言的語法, 在此之後,任何類、介面、初始化方法或成員的泛型簽名如果包含了型別變數(Type _Variables ) 或引數化矣型(Parameterized Types ) , 則Signature屬性會為它記錄適型簽名資訊。之所以要專門使用這樣一個屬性去記錄泛型型別,是因為Java語言的泛型採用的是擦除法實現的偽泛型,在位元組碼(Code屬性)中 ,泛型資訊編譯(型別變數、引數化型別)之後 都通通被擦除掉。使用擦除法的好處是實現簡單(主要修改Javac編 譯 器 ,虛擬機器內部只做
了很少的改動)、非常容易實現Backport , 執行期也能夠節省_ 些型別所佔的記憶體空間。但壞處是執行期就無法像C#等有真泛型支援的語言那樣,將泛型型別與使用者定義的普通型別同等對待 ,例如執行期做反射時無法獲得到泛型資訊。Signature屬性就是為了彌補這個缺陷而 增設的,現在Java的反射API能夠獲取泛型型別,最終的資料來源也就是這個屬性。關於Java泛型、Signature屬性和型別擦除,在第10章介紹編譯器優化的時候會通過一個具體的例子來 講解。Signature屬性的結構見表6-28。

其中signature_index項的值必須是一個對常量池的有效索引。常量池在該索引處的項必須 是CONSTANT_Utf8_info結構,表示類簽名、方法型別簽名或欄位型別簽名。如果當前的Signature屬性是類檔案的屬性,則這個結構表示類簽名,如果當前的Signature屬性是方法表的屬性 ,則這個結構表示方法型別簽名,如果當前Signature屬性是欄位表的屬性,則這個結構表示欄位型別簽名。

11.BootstrapMethods屬性

BootstrapMethods屬性在JDK 1.7釋出後增加到了Class檔案規範之中,它是一個複雜的變長屬性 ,位於類檔案的屬性表中。這個屬性用於儲存invokedynamic指令引用的引導方法限定符。 《 Java虛擬機器規範(Java SE 7版)》規定,如果某個類檔案結構的常量池中曾經出現過 CONSTANT_InvokeDymmic_info型別的常量,那麼這個類檔案的屬性表中必須存在一個明確的BootstrapMethods屬性 ,另外 ,即使CONSTANT_InvokeDymmic_info型別的常量在常量池中出現過多次,類檔案的屬表中最姜也只能有一個BootstrapMethods屬性。BootstrapMethods 屬性與JSR-292中fi^InvokeDymmic指令和java.lang.Invoke包關係非常密切,要介紹這個屬性的作用 ,必須先弄清楚InovkeDymmic指令的運作原理,筆者將在第8章專門用1節篇幅去介紹它們,在此先暫時略過。

目前的Javac暫時無法生成InvokeDynamic指令和BootstrapMethods屬性,必須通過一些非常規的手段才能使用到它們,也許在不久的將來,等JSR-292更加成熟一些,這種狀況就會改變。BootstrapMethods屬性的結構見表6-29。

其中引用到的bootstrap_method結構見表6-30。

BootstrapMethods屬性中, num_bootstrap_methods項的值給出了bootstrap_methods[]陣列中 的引導方法限定符的數量。而bootstrap_methods[]陣列的每個成員包含了一不指向常量池 CONSTANT_MethodHandle結構的索引值,它代表了 一個引導方法,還包含了這個引導方法 靜態引數的序列(可能為空)。bootstrap_methods[]陣列中的每個成員必須包含以下3項內容。

bootstrap_method_ref : bootstrap_method_ref項的值必須是一個對常量池的有效索引。常量池在該索引處的值必須是一個CONSTANT_MethodHandle_info結構。

num_bootstrap_arguments : num_bootstrap_arguments項的值給出了bootstrap_arguments[]陣列成員數量。

bootstrap_arguments[] : bootstrap_arguments[]陣列的每個成員必須是一個對常量池的有效索引。常量池”在該索引處必須是下列結構之一 : CONSTANT_String_info、
CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、 CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或 CONSTANT_MethodType_info.

注JDK1.4.2之前的Javac編譯器採用了jsr和ret指令實現finally語句,但1.4.2之後已經改為編譯器自動在每段可能的分支路徑之後都將finally語句塊的內容冗餘生成一遍來實現finally語 義。在JDK 1.7中 ,已經完全禁止Class檔案中出現jsr和ret指令,如果遇到這兩條指令,虛擬機器會在類載入的位元組碼校驗階段丟擲異常。

Class檔案結構的發展

Class檔案結構自Java虛擬機器規範第1版訂立以來,已經有十多年的歷史。這十多年間 ,Java技術體系有了翻天覆地的改變,JDK的版本號已經從1.0提升到了 1.7。相對於語言、API以及Java技術體系中其他方面的變化,Class檔案結構一直處於比較穩定的狀態,Class文 件的主體結構、位元組碼指令的語義和數量幾乎沒有出現過變動,所有對Class檔案格式的改進 ,都集中在向訪問標誌、屬性表這些在設計上就可擴充套件的資料結構中新增內容。

如果以《Java虛擬機器規範(第2版 )》為基準進行比較的話,那麼在後續Class檔案格式的發展過程中,訪問標誌裡新加入了ACC_SYNTHETiC、ACC_ANNOTATION、ACC_ENUM、ACC_BRIDGE、ACC_VARARGS共5個標誌。而屬性表集合中,在JDK 1.5到 JDK 1.7版本之間一共增加了12項新的屬性,這些屬性大部分用於支援Java中許多新出現的語言特性 ,如列舉、變長引數、泛型、動態註解等。還有一些是為了支援效能改進和除錯資訊 ,譬如JDK 1.6的新型別校驗器的StackMapTable屬性和對非Java程式碼除錯中用到的 SourceDebugExtension屬性。

Class檔案格式所具備的平臺中立(不依賴於特定硬體及作業系統)、緊湊、穩定和可擴充套件的特點,是Java技術體系實現平臺無關、語言無關兩項特性的重要支柱。

十餘年間,位元組碼的數量和語義只發生過屈指可數的幾次變動,例如,JDK1.0.2時改動過invokespecial指令的語義;JDK1.7增加了invokedynamic指令,禁止了ret和jsr指令。

相關文章