《深入理解Java虛擬機》-----第6章 類文件結構——Java高級開發必須懂的


代碼編譯的結果從本地機器碼轉變為字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步。

6.1 概述

記得在第一節計算機程序課上我的老師就講過:“計算機只認識0和1,所以我們寫的程序需要經編譯器翻譯成由0和1構成的二進制格式才能由計算機執行”。10多年時間過去了,今天的計算機仍然只能識別0和1,但由於最近10年內虛擬機以及大量建立在虛擬機之上的程序語言如雨后春筍般出現並蓬勃發展,將我們編寫的程序編譯成二進制本地機器碼(Native Code)已不再是唯一的選擇,越來越多的程序語言選擇了與操作系統和機器指令集無關的、平台中立的格式作為程序編譯后的存儲格式。

6.2 無關性的基石

如果計算機的CPU指令集只有x86一種,操作系統也只有Windows一種,那也許Java語言就不會出現。Java在剛剛誕生之時曾經提出過一個非常著名的宣傳口號:“一次編寫,到處運行(Write Once,Run Anywhere)”,這句話充分表達了軟件開發人員對沖破平台界限的渴求。在無時無刻不充滿競爭的IT領域,不可能只有Wintel存在,我們也不希望只有Wintel存在,各種不同的硬件體系結構和不同的操作系統肯定會長期並存發展。“與平台無關”的理想 
最終實現在操作系統的應用層上:Sun公司以及其他虛擬機提供商發布了許多可以運行在各種不同平台上的虛擬機,這些虛擬機都可以載入和執行同一種平台無關的字節碼,從而實現了程序的“一次編寫,到處運行”。

各種不同平台的虛擬機與所有平台都統一使用的程序存儲格式——字節碼(ByteCode)是構成平台無關性的基石,但本節標題中刻意省略了“平台”二字,那是因為筆者注意到虛擬機的另外一種中立特性——語言無關性正越來越被開發者所重視。到目前為止,或許大部分程序員都還認為Java虛擬機執行Java程序是一件理所當然和天經地義的事情。但在Java發展之初,設計者就曾經考慮過並實現了讓其他語言運行在Java虛擬機之上的可能性,他們在發布規范文檔的時候,也刻意把Java的規范拆分成了Java語言規范《The Java Language Specification》及Java虛擬機規范《The Java Virtual Machine Specification》。並且在1997年發布的第一版Java虛擬機規范中就曾經承諾過:“In the future,we will consider bounded extensions to the Java virtual machine to provide better support for other languages”(在未來,我們會對Java虛擬機進行適當的擴展,以便更好地支持其他語言運行於JVM之上),當Java虛擬機發展到JDK 1.7~1.8的時候,JVM設計者通過JSR-292基本兌現了這個承諾。

時至今日,商業機構和開源機構已經在Java語言之外發展出一大批在Java虛擬機之上運行的語言,如Clojure、Groovy、JRuby、Jython、Scala等。使用過這些語言的開發者可能還不是非常多,但是聽說過的人肯定已經不少,隨着時間的推移,誰能保證日后Java虛擬機在語言無關性上的優勢不會趕上甚至超越它在平台無關性上的優勢呢?

實現語言無關性的基礎仍然是虛擬機和字節碼存儲格式。Java虛擬機不和包括Java在內的任何語言綁定,它只與“Class文件”這種特定的二進制文件格式所關聯,Class文件中包含了Java虛擬機指令集和符號表以及若干其他輔助信息。基於安全方面的考慮,Java虛擬機規范要求在Class文件中使用許多強制性的語法和結構化約束,但任一門功能性語言都可以表示為一個能被Java虛擬機所接受的有效的Class文件。作為一個通用的、機器無關的執行平台,任何其他語言的實現者都可以將Java虛擬機作為語言的產品交付媒介。例如,使用Java編譯器可以把Java代碼編譯為存儲字節碼的Class文件,使用JRuby等其他語言的編譯器一樣可以把程序代碼編譯成Class文件,虛擬機並不關心Class的來源是何種語言,如圖6-1所示。

這里寫圖片描述

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

6.3 Class類文件的結構

解析Class文件的數據結構是本章的最主要內容。筆者曾經在前言中闡述過本書的寫作風格:力求在保證邏輯准確的前提下,用盡量通俗的語言和案例去講述虛擬機中與開發關系最為密切的內容。但是,對數據結構方面的講解不可避免地會比較枯燥,而這部分內容又是了解虛擬機的重要基礎之一。如果想比較深入地了解虛擬機,那么這部分是不能不接觸的。

在本章關於Class文件結構的講解中,我們將以《Java虛擬機規范(第2版)》(1999年發布,對應於JDK 1.4時代的Java虛擬機)中的定義為主線,這部分內容雖然古老,但它所包含的指令、屬性是Class文件中最重要和最基礎的。

注意 任何一個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)這樣的細節,都是被嚴格限定的,哪個字節代表什么含義,長度是多少,先后順序如何,都不允許改變。接下來我們將一起看看這個表中各個數據項的具體含義。

6.3.1 魔數與Class文件的版本

每個Class文件的頭4個字節稱為魔數(Magic Number),它的唯一作用是確定這個文件是否為一個能被虛擬機接受的Class文件。很多文件存儲標准中都使用魔數來進行身份識別,譬如圖片格式,如gif或者jpeg等在文件頭中都存有魔數。使用魔數而不是擴展名來進行識別主要是基於安全方面的考慮,因為文件擴展名可以隨意地改動。文件格式的制定者可以自由地選擇魔數值,只要這個魔數值還沒有被廣泛采用過同時又不會引起混淆即可。Class文件的魔數的獲得很有“浪漫氣息”,值為:0xCAFEBABE(咖啡寶貝?),這個魔數值在Java還稱做“Oak”語言的時候(大約是1991年前后)就已經確定下了。它還有一段很有趣的歷史,據Java開發小組最初的關鍵成員Patrick Naughton所說:“我們一直在尋找一些好玩的、容易記憶的東西,選擇0xCAFEBABE是因為它象征着著名咖啡品牌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文件為基礎來進行講解。

代碼清單6-1 簡單的Java代碼

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

 

圖6-2顯示的是使用十六進制編輯器WinHex打開這個Class文件的結果,可以清楚地看見開頭4個字節的十六進制表示是0xCAFEBABE,代表次版本號的第5個和第6個字節值為0x0000,而主版本號的值為0x0032,也即是十進制的50,該版本號說明這個文件是可以被JDK 1.6或以上版本虛擬機執行的Class文件。 
這里寫圖片描述

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

6.3.2 常量池

緊接着主次版本號之后的是常量池入口,常量池可以理解為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,本章不會涉及這3種新增的類型,在第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/fenixsoft/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 使用Javap命令輸出常量表

C:\>javap-verbose TestClass
Compiled from "TestClass.java"
public class org.fenixsoft.clazz.TestClass extends java.lang.Object
SourceFile:"TestClass.java"
minor version:0
major version:50
Constant pool:
const#1=class#2;//org/fenixsoft/clazz/TestClass
const#2=Asciz org/fenixsoft/clazz/TestClass;
const#3=class#4;//java/lang/Object
const#4=Asciz java/lang/Object;
const#5=Asciz m;
const#6=Asciz I;
const#7=Asciz<init>;
const#8=Asciz()V;
const#9=Asciz Code;
const#10=Method#3.#11;//java/lang/Object."<init>":()V
const#11=NameAndType#7:#8;//"<init>":()V
const#12=Asciz LineNumberTable;
const#13=Asciz LocalVariableTable;
const#14=Asciz thisconst#15=Asciz Lorg/fenixsoft/clazz/TestClass;
const#16=Asciz inc;
const#17=Asciz()I;
const#18=Field#1.#19;//org/fenixsoft/clazz/TestClass.m:I
const#19=NameAndType#5:#6;//m:I
const#20=Asciz SourceFile;
const#21=Asciz TestClass.java;

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

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

6.3.3 訪問標志

在常量池結束之后,緊接着的兩個字節代表訪問標志(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個標志應當為假,因此它的access_flags的值應為:0x0001|0x0020=0x0021。從圖6-5中可以看出,access_flags標志(偏移地址:0x000000EF)的確為0x0021。 
這里寫圖片描述

6.3.4 類索引、父類索引與接口索引集合

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

類索引、父類索引和接口索引集合都按順序排列在訪問標志之后,類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個類型為CONSTANT_Class_info的類描述符常量,通過CONSTANT_Class_info類型的常量中的索引值可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串。圖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所示。

代碼清單6-3 部分常量池內容

const#1=class#2;//org/fenixsoft/clazz/TestClass
const#2=Asciz org/fenixsoft/clazz/TestClass;
const#3=class#4;//java/lang/Object
const#4=Asciz java/lang/Object;

6.3.5 字段表集合

字段表(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。關於attribute_info的其他內容,將在6.3.7節介紹屬性表的數據項目時再進一步講解。 
這里寫圖片描述

字段表集合中不會列出從超類或者父接口中繼承而來的字段,但有可能列出原本Java代碼之中不存在的字段,譬如在內部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。另外,在Java語言中字段是無法重載的,兩個字段的數據類型、修飾符不管是否相同,都必須使用不一樣的名稱,但是對於字節碼來講,如果兩個字段的描述符不一致,那字段重名就是合法的。

6.3.6 方法表集合

如果理解了上一節關於字段表的內容,那本節關於方法表的內容將會變得很簡單。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”,說明此屬性是方法的字節碼描述。 
這里寫圖片描述 
與字段表集合相對應的,如果父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現來自父類的方法信息。但同樣的,有可能會出現由編譯器自動添加的方法,最典型的便是類構造器“<clinit>”方法和實例構造器“<init>”方法。

在Java語言中,要重載(Overload)一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的特征簽名,特征簽名就是一個方法中各個參數在常量池中的字段符號引用的集合,也就是因為返回值不會包含在特征簽名中,因此Java語言里面是無法僅僅依靠返回值的不同來對一個已有方法進行重載的。但是在Class文件格式中,特征簽名的范圍更大一些,只要描述符不是完全一致的兩個方法也可以共存。也就是說,如果兩個方法有相同的名稱和特征簽名,但返回值不同,那么也是可以合法共存於同一個Class文件中的。(Java代碼的方法特征簽名只包括了方法名稱、參數順序及參數類型,而字節碼的特征簽名還包括方法返回值以及受查異常表)

6.3.7 屬性表集合

屬性表(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和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條編碼值對應的指令含義,編碼與指令之間的對應關系可查閱本書的附錄B“虛擬機字節碼指令表”。

關於code_length,有一件值得注意的事情,雖然它是一個u4類型的長度值,理論上最大值可以達到2(32次方)-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所示。

代碼清單6-4 用javap命令計算字節碼指令

//原始Java代碼
public class TestClass{
private int m;
public int inc(){
return m+1;
}}C
:\>javap-verbose TestClass
//常量表部分的輸出見代碼清單6-1,因版面原因這里省略掉
{
public org.fenixsoft.clazz.TestClass();
Code:
Stack=1,Locals=1,Args_size=1
0:aload_0
1:invokespecial#10;//Method java/lang/Object."<init>":()V
4:return
LineNumberTable:
line 3:0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/fenixsoft/clazz/TestClass;
public int inc();
Code:
Stack=2,Locals=1,Args_size=1
0:aload_0
1:getfield#18;//Field m:I
4:iconst_1
5:iadd
6:ireturn
LineNumberTable:
line 8:0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lorg/fenixsoft/clazz/TestClass;
}

如果大家注意到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源碼,想一下這段代碼的返回值在出現異常和不出現異常的情況下分別應該是多少?

代碼清單6-5 異常表運作演示

//Java源碼
public int inc(){
int x;
try{
x=1return x;
}catch(Exception e){
x=2return x;
}finally{
x=3;
}}
//編譯后的ByteCode字節碼及異常表
public int inc();
Code:
Stack=1,Locals=5,Args_size=1
0:iconst_1//try塊中的x=1
1:istore_1
2:iload_1//保存x到returnValue中,此時x=1
3:istore 4
5:iconst_3//finaly塊中的x=3
6:istore_1
7:iload 4//將returnValue中的值放到棧頂,准備給ireturn返回
9:ireturn
10:astore_2//給catch中定義的Exception e賦值,存儲在Slot 2中
11:iconst_2//catch塊中的x=2
12:istore_1
13:iload_1//保存x到returnValue中,此時x=2
14:istore 4
16:iconst_3//finaly塊中的x=3
17:istore_1
18:iload 4//將returnValue中的值放到棧頂,准備給ireturn返回
20:ireturn
21:astore_3//如果出現了不屬於java.lang.Exception及其子類的異常才會走到這里
22:iconst_3//finaly塊中的x=3
23:istore_1
24:aload_3//將異常放置到棧頂,並拋出
25:athrow
Exception table:
from to target type
0 5 10 Class java/lang/Exception
0 5 21 any
10 16 21 any

編譯器為這段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_number_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兩個。

順便提一下,在JDK 1.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_String_info常量中的一種。

7.InnerClasses屬性

InnerClasses屬性用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那編譯器將會為它以及它所包含的內部類生成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型常量的索引,分別代表了內部類和宿主類的符號引用。

inner_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。 
這里寫圖片描述

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

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_InvokeDynamic_info類型的常量,那么這個類文件的屬性表中必須存在一個明確的BootstrapMethods屬性,另外,即使CONSTANT_InvokeDynamic_info類型的常量在常量池中出現過多次,類文件的屬性表中最多也只能有一個BootstrapMethods屬性。BootstrapMethods屬性與JSR-292中的InvokeDynamic指令和java.lang.Invoke包關系非常密切,要介紹這個屬性的作用,必須先弄清楚InovkeDynamic指令的運作原理,筆者將在第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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM