JVM筆記9-Class類文件結構

摘要: 1.Class類文件結構    Class 文件是一組以 8 位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有添加任何分隔符,這使得整個 Class 文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。java

1.Class類文件結構 bootstrap

  Class 文件是一組以 8 位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有添加任何分隔符,這使得整個 Class 文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。小程序

  當遇到須要佔用 8 位字節以上空間的數據項時,則會按照高位在前(Big-Endian)的方式分割成若干個 8 位字節進行存儲。數組

          根據 Java 虛擬機規範的規定,Class 文件格式採用一種相似於 C  語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表,後面的解析都要以這兩種數據類型爲基礎,因此這裏要先介紹這兩個概念。安全

          無符號數屬於基本的數據類型,以 u一、u二、u四、u8 來分別表明 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數,無符號數能夠用來描述數字、索引引用、數量值或者按照 UTF-8 編碼構成字符串值。數據結構

          表是由多個無符號數或者其餘表做爲數據項構成的複合數據類型,全部表都習慣性地以 「_info」 結尾。表用於描述有層次關係的複合結構的數據,整個 Class 文件本質上就是一張表,它由表 6-1 所示的數據項構成。併發

  

                                                                   表6-1編輯器

    不管是無符號數仍是表,當須要描述同一類型但數量不定的多個數據時,常常會使用一個前置的容量計數器,加若干個連續的數據項的形式,這時稱這一系列的某一類型的數據爲某一類型的集合。工具

  Class的結構不像XML等描述語言,因爲它沒有任何分隔符號,因此在表6-1的數據項,不管是順序仍是數量,甚至於數據存儲的字節序(Byte Ordering,Class 文件中字節序爲 Big-Endian)這樣的細節,佈局

  都是被嚴格限定的,哪一個字節表明什麼含義,長度是多少,前後順序如何,都不容許改變。

 

2.魔數與Class文件的版本

  每一個Class文件的頭4個字節稱爲魔數,它的惟一做用是肯定這個文件是否爲一個能被虛擬機接受的Class文件。不少文件儲存標準中都使用魔數來進行身份識別,譬如圖片格式,如 gif 或者 jpeg 等在文件頭中都存有魔數。

使用魔數而不是擴展名來僅從識別主要是基於安全方面的考慮,由於文件擴展名能夠隨意地改動。文件格式的制定者額能夠自由地選擇魔數值,只要這個魔數值尚未被普遍採用過同時又不會引發混淆便可。

Class 文件的魔數的得到頗有「浪漫氣息」,值爲:0xCAFEBABE(咖啡寶貝?),這個魔數值再 Java 還稱作「Oak」語言的時候(大約是 1991 年先後)就已經肯定下來了。

  緊接着魔數的4個字節儲存的是Class文件的版本號:第5和第6個字節是次版本號,第7和第8個字節是主版本號。Java版本號是從45開始的,JDK1.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.

  後面的內容都將以這段小程序使用 JDK 1.6 編譯輸出的 Class 文件爲基礎來進行講解。以下:

package org.fenixsoft.clazz;  
  
public class TestClass {  
  
    private int m;  
      
    public int inc() {  
        return m + 1;  
    }  
}

下圖顯示的是使用十六進制編輯器WinHex打開這個Class文件的結果,能夠清除的看見開頭4個字節的十六進制表示是0xCAFEBABE,表明此版本號的第5和第6個字節值爲0x0000,而主版本號的值爲0x0032,也即十進制的50,

改版本號說明這個文件是能夠被JDK1.6或以上版本虛擬機執行的Class文件。

表 6-2 列出了從 JDK 1.1 到 JDK 1.7,主流 JDK 版本編譯器輸出的默認和可支持的 Class 文件版本號。

3.常量池

  緊接着主次版本號以後的是常量池入口,常量池能夠理解爲Class文件之中的資源倉庫,它是Class文件結構中與其餘項目關聯最多的數據類型,也是佔用Class文件空間最大的數據項目之一,

同時它仍是在Class文件中第一個出現的表類型數據項目。

  因爲常量池中常量的數量是不固定的,因此在常量池的入口須要設置一項u2類型的數據,表明常量池容量計數值。與Java中語言習慣不同的是,這個容量技術是從1而不是0開始的,以下圖表示,常量池容量(偏移地址:0x00000008)

爲十六進制數0x0016,即十進制22,這表明常量池中有21項常量,索引值範圍爲1~21。在Class文件格式規範制定之時,設計者將第0項常量空出來是有特殊考慮的,這樣作的目的在於知足後面某些指向常量池的索引值的數據在特定狀況下須要表達

"不引用任何一個常量池項目"的含義,這種狀況就能夠把索引值置爲0來表示。Class文件結構中只有常量池的容量計數從1開始,對於其餘集合類型,包括接口索引結合,字段表集合,方法表集合等容量技術都與通常習慣相同,從0開始。

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

   1.類和接口的全限定名

   2.字段的名稱和描述符

   3.方法的名稱和描述符

Java代碼在進行Javac編譯的時候,並不像C和C++那樣有「鏈接」這一步驟,而是在虛擬機加載Class文件的時候進行動態鏈接。也就是說,在Class文件中不會保存各個方法,字段的最終內存佈局信息,所以這些字段,方法的符號引用不通過運行期轉換的話沒法獲得真正的內存

入口地址,也就沒法直接被虛擬機使用。當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立時或運行時解析,翻譯到具體的內存地址中。

  常量池中每一項都是一個表,在JDK1.7以前共有11中結構不相同的表結構數據,在 JDK 1.7 中爲了更好地支持動態語言調用,又額外增長了3 種(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)

  這14種表都有一個共同的特色,就是表開始的第一位是一個u1類型的標誌位(tag,取值見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編碼的區別是:

從 '\u0800' 到 '\u07ff' 之間的全部字符的縮略編碼用兩個字節表示,從 '\u0800' 到 '\uffff' 之間的全部字符的縮略編碼接按照普通 UTF-8 編碼規則使用三個字節表示。

  順便提一下,因爲Class文件中方法,字段等都須要引用CONSTAN_Utf8_info型常量來描述名稱,因此CONSTAN_Utf8_info類型常量的最大長度也就是Java方法,字段名的最大長度。而這裏的最大長度就是length的最大值,既u2類型能表達的最大值65535.

因此Java程序中若是定義了超過64KB英文字符的變量和方法名,將會沒法編譯。

  本例中這個字符串的length值(偏移地址:0x0000000E)爲 0x001D,也就是長 29 字節,日後 29 字節正好都在 1 ~ 127 的 ASCII 碼範圍之內,內容爲 「org/fenixsoft/clazz/TestClass」。一個個字節換算一下,換算結果以下:

到此爲止,咱們分析了 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 以供讀者參考。

 

4.訪問標誌

  在常量池結束以後,緊接着的兩個字節表明訪問標誌(access_flags),這個標誌用於識別一些類或接口層次的訪問信息,包括:這個Class是類仍是接口;是否認義爲public類型;是否認義爲abstrack類型;若是是類的話,

是否被聲明爲final等。具體的標誌位以及標誌的含義見表 6-7。

    access_flags 中一共有 16 個標誌位可使用,當前只定義了其中 8 個(注:在 Java 虛擬機規範中,只定義了開頭 5 種標誌。JDK 1.5 中增長了後面三種。這些標誌位在 JSR-202 規範中聲明。),

沒有使用到的標誌位要求一概爲 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 個標誌應當爲假,

所以它的 access_flags 值應爲:0x0001 | 0x0020 = 0x0021。從圖 6-5 中能夠看出,access_flags 標誌(偏移地址:0x000000EF)的確爲 0x0021。

5.類索引,父索引與接口索引集合

   類索引(this_class)和父類索引(super_class)都是一個u2 類型的數據,而接口索引集合(interfaces)是一組 u2 類型的數據的集合,Class文件中由這三項數據來肯定這個類的繼承關係。類索引用於肯定這個類的全限定名,

父類索引用於肯定這個類的弗雷的全限定名。因爲Java語言不容許多重繼承,因此父類索引只有一個,除了java.lang.Object 外,全部 Java 類的父類索引都不爲 0。接口索引集合就用來描述這個類實現了哪些接口,這些被實現的接口就按implements語句

(若是這個類自己是一個接口,則應當是 extends 語句)後的接口順序從左到右排列在接口索引集合中。

  類索引,父類索引和接口索引集合都按照順序排列在訪問標誌以後,類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個類型爲CONSTANT_Class_info的類描述符常量,經過CONSTANT_Class_info類型的常量中的索引值能夠

找到定義在CONSTAN_Utif8_info類型的常量中的全限定名字符串。圖6-6表演了上面代碼的類索引查找過程。

  對於接口索引集合,入口的第一項——u2類型的數據爲接口計數器表示索引表的容量。若是該類沒有實現任何接口,則該技術值爲0,後面接口的索引表再也不佔用任何字節。代碼清單6-1中的代碼的類索引,父類索引與接口表索引的內容如圖6-7因此

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

6.字段表集合

字段表(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;」,一個整型數組 「int[]」 將被記錄爲 「[I」。

  用描述符來描述方法時,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號「()」以內。如方法void inc()的描述符爲「()V」,

方法 java.lang.String toString() 的描述符爲 「()Ljava/lang/String;」,方法 int indexOf(char[] source, int sourceOffset, int sourceCount, char[]target, int targetOffset, int targetCount, int fromIndex)的描述符爲 「([CII[CIII)I」。

  

對於代碼清單 6-1 中的 TestClass.class 文件來講,字段表集合從地址 0x000000F8 開始,第一個 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。

   字段表集合中不會列出從超類或者父接口中繼承而來的字段,但有可能列出本來 Java 代碼之中不存在的字段,譬如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段。

另外,在 Java 語言中字段是沒法被重載的,兩個字段的數據類型、修飾符無論是否相同,都必須使用不同的名稱,可是對於字節碼來將,若是兩個字段的描述符不一致,那字段重名就是合法的。

 

7.方法集合

  若是理解了上面字段表的內容,那麼方法表的內容將會變得很簡單。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 文件格式中最具擴展性的一種數據項目,將在下一節(屬性表集合)中詳細講解。

  繼續以上面的代碼清單中的Class文件爲例對方法集合進行分析,如6-9所示,方法集合的入口地址爲0x00000101,第一個u2類型的數據(便是計數器容量)的值爲0x0002,表明集合中有兩個方法(這兩個方法爲編譯器添加的實例構造<init>和源碼中的方法inc())。

第一個方法的訪問標誌值爲 0x0001,也就是隻有 ACC_PUBLIC 標誌爲真,名稱索引值爲 0x0007,查代碼清單 6-2 的常量池得方法名爲「<init>」,描述符索引值爲 0x0008,對應常量爲「()V」,屬性表計數器 attributes_count 的值爲 0x0001 就表示此方法的屬性表集合有一項屬性,屬性名稱索引爲 0x0009,對應常量爲「Code」,說明此屬性是方法的字節碼描述。

  與字段表集合相對應的,若是父類方法在子類中沒有被重寫,方法集合中就不會出現來自父類的方法信息。但一樣的,有可能會出現由編譯器自動添加的方法,最經型的即是類構造器"<clinit>"方法和實例構造器「<init>」方法。

   在 Java 語言中,要重載(Overload)一個方法,除了要與原方法具備相同的簡單名稱以外,還要求必須擁有一個與原方法不一樣的特徵簽名(注:Java 代碼的方法特徵簽名只包括方法名稱、參數順序及參數類型,而字節碼的特徵簽名還包括方法返回值以及受查異常表),特徵簽名就是一個方法中各個參數在常量池中的字段符號引用的集合,也就是由於返回值不會包含在特徵簽名中,所以 Java 語言裏面是沒法僅僅依靠返回值的不一樣來對一個已有方法進行重載的。可是在 Class 文件格式中,特徵簽名的範圍更大一些,只要描述符不是徹底一致的兩個方法也能夠共存。也就是說,若是兩個方法有相同的名稱和特徵簽名,但返回值不一樣,那麼也是能夠合法共存於同一個 Class 文件中的。

 

8.屬性表集合

      屬性表(attribute_info)在前面的講解之中已經出現過數次,在 Class 文件、字段表、方法表均可以攜帶子機的屬性表集合,以用於描述某些場景專有的信息。

與 Class 文件中其餘的數據項目要求嚴格的順序、長度和內容不一樣,屬性表集合的限制稍微寬鬆了一些,再也不要求各個屬性表具備嚴格順序,而且只要不與已有屬性名重複,任何人實現的編譯器均可以向屬性表中寫入本身定義的屬性信息,

Java 虛擬機運行時會忽略掉它不認識的屬性。爲了能正確解析 Class 文件,《Java 虛擬機規範(第 2 版)》中預約義了 9 項虛擬機實現應當能識別的屬性,而在最新的《Java 虛擬機規範(Java SE 7)》版中,

預約義屬性已經增長到 21 項,具體內容見表 6-13。下文中將對其中一些屬性中的關鍵經常使用的部分進行講解。

                      6-13

 

 

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

 

9.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 和 returnAddress 等長度不超過 32 位的數據類型,每一個局部變量佔用 1 個 Slot,

而 double 和 long 這兩種 64 位的數據類型則須要兩個 Slot 來存放。方法參數(包括實例方法中的隱藏參數 「this」)、顯式異常處理器的參數(Exception Handler Parameter,就是 try-catch 語句中 catch 塊所定義的異常)、方法體中定義的局部變量都須要使用局部變量表來存放。

另外,並非在方法中用到了多少個局部變量,就把這些局部變量所佔 Slot 之和做爲 max_locals 的值,緣由是局部變量表中的 Slot 能夠重寫,當代碼執行超出一個局部變量的做用域時,這個局部變量所佔的 Slot 能夠被其餘局部變量所使用,

Javac 編譯器會根據變量的做用域來分配 Slot 給各個變量使用,而後計算出 max_locals 的大小。

    code_length 和 code 用來存儲 Java 源程序編譯後生成的字節碼指令。code_length 表明字節碼長度,code 是用於存儲字節碼指令的一系列字節流。既然叫字節碼指令,那麼每一個指令就是一個 u1 類型的單字節,當虛擬機讀取到 code 中的一個字節碼時,

就能夠對應找出這個字節碼錶明的是什麼指令,而且能夠知道到這條指令後面是否須要跟隨參數,以及參數應當如何理解。咱們知道一個 u1 數據類型的取值範圍爲 0x00 ~ 0xFF,對應十進制的 0 ~ 255,也就是一共能夠表達 256 條指令,目前,Java 虛擬機規範已經定義了其中約 200 條編碼值對應的指令含義。

    關於 code_length,有一件值得注意的事情,雖然它是一個 u4 類型的長度值,理論上最大值能夠達到 2^23-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)後面還會帶有參數。
        咱們再次使用 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 行(注:此處字節碼的 「行」 是一種形象的描述,指的是字節碼相對於方法體開始的偏移量,

而不是 Java 源碼的行號)到第 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,並將棧頂的異常拋出,方法結束。
        儘管你們都知道這段代碼出現異常的機率很是小,但並不影響咱們演示異常表的做用。

10.Exception屬性

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

 

11.LineNumberTable屬性

  LineNumberTable 屬性用於描述 Java 源碼行號與字節碼行號(字節碼的偏移量)之間的對應關係。它並非運行時必需的屬性,但默認會生成到 Class 文件之中,能夠在 javac 中分別使用 -g:none 或 -g:lines 選項來取消或要求生成這項信息。若是選擇不生成LineNumberTable 屬性,對程序運行產生的最主要的影響就是當拋出異常時,堆棧中將不會顯示出錯的行號,而且在調試程序的時候,也沒法按照源碼行來設置斷點。LineNumberTable 屬性的結構見表 6-18。

 line_number_table  是一個數量爲 line_number_table_length、類型爲 line_number_info 的集合,line_number_info 表包括了 start_pc 和 line_number 兩個 u2 類型的數據項,前者是字節碼行號,後者是 Java  源碼行號。

 

12.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 兩個。

        順便提一下,在 JDK 1.5 引入泛型以後,LocalVariableTable 屬性增長了一個 「姐妹屬性」:LocalVariableTypeTable,這個新增的屬性結構與 LocalVariableTable 很是類似,僅僅是把記錄的字段描述符的 descriptor_index 替換成了字段的特徵簽名(Signature),

對於非泛型類型來講,描述符和特徵簽名能描述的信息是基本一致的,可是泛型引入以後,因爲描述符中泛型的參數化類型被擦除掉,描述符就不能準確地描述泛型類型了,所以出現了 LocalVariableTypeTable。

 

12.SourceFile屬性

   SourceFile 屬性用於記錄生成這個 Class 文件的源碼文件名稱。這個屬性也是可選的,能夠分別使用 javac 的 -g:none 或 -g:source 選項來關閉或要求生成這項信息。在 Java 中,對於大多數的類來講,類名和文件名是一致的,可是又一些特殊狀況(如內部類)例外。若是不生成這項屬性,當拋出異常時,堆棧中將不會顯示出錯代碼所屬的文件名。這個屬性是一個定長的屬性,其結構見表 6-21。

sourcefile_index 數據項是指向常量池中 CONSTANT_Utf8_info 型常量的索引,常量值是源碼文件的文件名。

 

13.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_String_info常量中的一種。

 

14.InnerClass屬性

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

 數據項 number_of_classes 表明須要記錄多少個內部類信息,每個內部類的信息都由一個 inner_classes_info 表進行描述。inner_classes_info 表的結構見表 6-24。

  inner_class_info_index 和 outer_class_info_index 都是指向常量池中 CONSTANT_Class_info 型常量的索引,分別表明了內部類和宿主類的符號引用。

        inner_name_inex 是指向常量池中 CONSTANT_Utf8_info 型常量的索引,表明這個內部類的名稱,若是是匿名內部類,那麼這項值爲 0。

        inner_class_access_flags 是內部類的訪問標誌,相似於類的 access_flags,它的取值範圍見表 6-25。

 

15.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,由於沒有任何屬性值須要設置

 

16.StackMapTable屬性

  

StackMapTable 屬性在 JDK 1.6 發佈後增長到了 Class 文件規範中,它是一個複雜的變長屬性,位於 Code 屬性的屬性表中。這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(Type Checker)使用,目的在於代替之前比較消耗性能的基於數據流分析的類型推導驗證器。

        這個類型檢查驗證器最初來源於 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。

《Java 虛擬機規範(Java SE 7 版)》明確規定:在版本號大於或等於 50.0 的 Class 文件中,若是方法的 Code 屬性中沒有附帶 StackMapTable 屬性,那就意味着它帶有一個隱式的 StackMap 屬性。這個 StackMap 屬性的做用等同於 number_of_entries 值爲 0 的 StackMapTable 屬性。一個方法的 Code 屬性最多隻能有一個 StackMapTable 屬性,不然將拋出 ClassFormatError 異常。

 

17.Signature屬性

   Signature 屬性在 JDK 1.5 發佈後增長到了 Class 文件規範之中,它是一個可選的定長屬性,能夠出現於類、屬性表和方法表結構的屬性表中。在 JDK 1.5 中大幅加強了 Java 語言的語法,在此以後,任何類、接口、初始化方法或成員的泛型簽名若是包含了類型變量(Type Variables)或參數化類型(Parameterized Types),則 Signature 屬性會爲它記錄泛型簽名信息。之因此要專門使用這樣一個屬性去記錄泛型類型,是由於 Java 語言的泛型採用的是擦除法實現的僞泛型,在字節碼(Code 屬性)中,泛型信息編譯(類型變量、參數化類型)以後都統統被擦除掉。使用擦除法的好處是實現簡單(主要修改 Javac 編譯器,虛擬機內部只作了不多的改動)、很是容易實現 backport,運行期也可以節省一些類型所佔的內存空間。但壞處是運行期就沒法像 C# 等有真泛型支持的語言那樣,將泛型類型與用戶定義的普通類型同等對待,例如運行期作反射時沒法得到泛型信息。Signature 屬性就是爲了彌補這個缺陷而增設的,如今 Java 的反射 API 可以獲取泛型類型,最終的數據來源也就是這個屬性。Signature 屬性的結構見表 6-28。

 其中 signature_index 項的值必須是一個對常量池的有效索引。常量池在該索引處的項必須是 CONSTANT_Utf8_info 結構,表示類簽名、方法類型簽名或字段類型簽名。若是當前 Signature 屬性是類文件的屬性,則這個結構表示類簽名,若是當前的 Signature 屬性是方法表的屬性,則這個結構表示方法類型簽名,若是當前 Signature 屬性是字段表的屬性,則這個結構表示字段類型簽名。

 

18.BootstrapMethods屬性 

    BootstrapMethods 屬性在 JDK 1.7 發佈後增長到了 Class 文件規範之中,它是一個複雜的變長屬性,位於類文件的屬性表中。這個屬性用於保存 invokedynamic 指令引用的引導方法限定符。《Java 虛擬機規範(Java SE 7 版)》規定,若是某個類文件結構的常量池中曾經出現過 CONSTANT_InvokeDynamic_info 類型的常量,那麼這個類文件的屬性表中必須存在一個明確的 BootstrapMethods 屬性,另外,即便 CONSTANT_InvokeDynamic_info 類型的常量在常量池中出現過屢次,類文件的屬性表中最多也只能有一個 BootstrapMethods 屬性。BootstrapMethods 屬性與 JSR-292 中的 InvokeDynamic 指令和 java.lang.invoke 包關係很是密切,要介紹這個屬性的做用,必須先弄清楚 InvokeDynamic 指令的運做原理,在此先暫時略過。

        目前的 javac 暫時沒法生成 InvokeDynamic 指令和 BootstrapMethods 屬性,必須經過一些很是規的手段才能使用到它們,也許在不就的未來,等 JSR-292 更加成熟一些,這種情況就會改變。BootstrapMethods 屬性的結構見表 6-29。

 BootstrapMethods 屬性中,num_bootstrap_methods 項的值給出了 bootstrap_methods[] 數組中的引導方法限定符的數量。而 bootstrap_methods[] 數組的每一個成員包含了一個指向常量池 CONSTANT_MethodHandle 結構的索引值,它表明了一個引導方法,還包含了這個引導方法靜態參數uDelay序列(可能爲空)。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。
相關文章
相關標籤/搜索