【深入理解 Java 虛擬機器筆記】類檔案結構
5.類檔案結構
由於最近十年內虛擬機器以及大量建立在虛擬機器之上的程式語言如雨後春筍般出現並蓬勃發展,將我們編寫的程式編譯成二進位制本地機器碼(Native Code)已不再是唯一的選擇,越來越多的程式語言選擇了作業系統和機器指令集無關的、平臺中立的格式作為程式編譯後的儲存格式。
無關性的基石
Java 剛誕生的宣傳口號:一次編寫,到處執行(Write Once, Run Anywhere)。其最終實現在作業系統的應用層:Sun公司以及其他虛擬機器提供商釋出了許多可以執行在各種不同平臺的虛擬機器,這些虛擬機器都可以載入和執行同一種平臺無關的位元組碼。
位元組碼(ByteCode)是構成平臺無關的基石。虛擬機器的語言無關性也越來越被開發者所重視,JVM 設計者在最初就考慮過實現讓其他語言執行在Java虛擬機器之上的可能性,如今已發展出一大批在 JVM 上執行的語言,比如 Clojure、Groovy、JRuby、Jython、Scala。
實現語言無關性的基礎仍是虛擬機器和位元組碼儲存格式,Java 虛擬機器不和包括 Java 在內的任何語言繫結,它只與 Class 檔案這種特定的二進位制檔案格式所關聯,這使得任何語言的都可以使用特定的編譯器將其原始碼編譯成 Class 檔案,從而在虛擬機器上執行。
Class 類檔案的結構
Class 檔案是一組以 8 個位元組為基礎單位的二進位制流(可能是磁碟檔案,也可能是類載入器直接生成的),各個資料專案嚴格按照順序緊湊地排列,中間沒有任何分隔符。
Class 檔案格式採用一種類似於 C 語言結構體的偽結構來儲存資料,其中只有兩種資料型別:
-
無符號數屬於基本的資料型別,以 u1、u2、u4 和 u8 來分別代表 1 個位元組、2 個位元組、4 個位元組和8 個位元組的無符號數,可以用來描述數字、索引引用、數量值或者按照 UTF-8 編碼構成字串值
-
表是由多個無符號數或者其他表作為資料項構成的複合資料型別,習慣以“_info”結尾。表用於描述有層次關係的複合結構資料,整個 Class 檔案本質上就是一張表。
Class 檔案格式:
無論是無符號數還是表,當需要描述同一個型別但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的資料項的形式,這時稱這一系列連續的某一型別的資料為某一型別的集合。
寫一個簡單的程式來分析 Class 檔案結構:
package com.chen;
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
通過 WinHex 開啟 Class 檔案:
魔數與 Class 檔案的版本
Class檔案的頭 4 個位元組被稱為魔數 (Magic Number),唯一作用是確定檔案是否為一個可被虛擬機器接受的 Class 檔案,固定為“0xCAFEBABE”。
第 5 和第 6 個位元組是次版本號(Minor Version),第 7 和第 8 個位元組是主版本號(Major Version),Java 版本號是從 45 開始的,JDK 1.1 之後每個大版本釋出,主版本向上加1(JDK 1.0~1.1使用了 45.0~45.3)。Java 能向下相容之前的版本,無法執行後續的版本。我們舉的例子中,0x0034 即 52,對應 JDK 1.8。
常量池
緊接著就是常量池入口,常量池可以理解為 Class 檔案之中的資源倉庫,是 Class 檔案結構中與其他專案關聯最多的資料型別,也是佔用 Class 檔案空間最大的資料項之一。
由於常量池中的常量數量不固定,因此需要在常量池前放置一項 u2 型別的資料,來表示常量池容量計數值(constant_pool_count)。該值是從 1 開始的,例子的 0x0016 為十進位制的 22,代表常量池中有 21 項常量,索引值範圍為1~21。設計者將第 0 項常量空出來,目的是滿足後面某些指向常量池的索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的含義。
常量池主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References):
- 字面量比較接近 Java 語言層次中的常量概念,如文字字串,宣告為 final 的常量值等
- 符號引用則屬於編譯原理方面的概念,包括三類常量:
- 類和介面的全限定名(Fully Qualified Name)
- 欄位的名稱和描述符(Descriptor)
- 方法的名稱和描述符
Java 程式碼在 javac 編譯時不會有“連線”這一步驟,而是在虛擬機器載入 Class 檔案的時候進行動態連線。所以 Class 檔案不會儲存各個方法、欄位和最終記憶體佈局資訊,必須經過執行期轉換,才能得到真正記憶體入口地址。當虛擬機器執行時需要從常量池獲取對應的符號引用,再在類建立時或執行時解析、翻譯到具體的記憶體地址中。
常量池中每一項常量都是一個表,JDK 1.7 中常量池共有 14 種不同的表結構資料,這些表結構開始的第一位是一個 u1 型別的標誌位,代表當前常量的型別,具體如下圖所示:
結合下圖中各個表結構的說明和之前使用 javap 解析的檔案內容:
例子中,第一個常量是 0x0A,屬於 CONSTANT_Methodref_info 型別,0x0004,指向第四項 CONSTANT_Class_info 常量,而0x0012 指向第18項 CONSTANT_NameAndType_info 常量。
第二個常量是 0x09,對應的是 CONSTANT_Fieldref_info 型別,0x0003,指向第三項 CONSTANT_Class_info 常量,而0x0013指向19項 CONSTANT_NameAndType_info 常量。
其餘的常量都可以通過類似的方法計算出來,計算機可以幫我們完成這一步,通過 javap,使用帶 -verbose 引數輸出的位元組碼內容:
從圖中可以看出,有一些常量從來沒有在程式碼出現過,如“I”、“V”、“”、“LineNumberTable”、“LocalVariableTable”等,這些自動生成的常量,會在欄位表(field_info)、方法表(method_info)、屬性表(attribute_info)引用到,用來描述一些不方便使用“固定位元組”進行表述的內容。
訪問標誌
在常量池結束之後,接著就是兩個位元組的訪問標誌(acess_flags),用於識別一些類或者介面層次的資訊。access_flags 中一共有16個標誌位可以使用,當前只定義了 8 個,沒有使用的一律為 0。
具體的標誌位:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x00 01 | 是否為 Public 型別 |
ACC_FINAL | 0x00 10 | 是否被宣告為 final,只有類可以設定 |
ACC_SUPER | 0x00 20 | 是否允許使用 invokespecial 位元組碼指令的新語意,invokespecial 在JDK 1.0.2 發生過改變,在 JDK 1.0.2 之後編譯出來的類這個標誌都必須為真 |
ACC_INTERFACE | 0x02 00 | 標誌這是一個介面 |
ACC_ABSTRACT | 0x04 00 | 是否為 abstract 型別,對於介面或者抽象類來說,此標誌值為真,其他型別為假 |
ACC_SYNTHETIC | 0x10 00 | 標誌這個類並非由使用者程式碼產生 |
ACC_ANNOTATION | 0x20 00 | 標誌這是一個註解 |
ACC_ENUM | 0x40 00 | 標誌這是一個列舉 |
我們例子中的 TestClass 是一個普通的 Java 類,不是介面,註解或列舉,被 public 修飾但不是 final 和 abstract,所以它的 ACC_PUBLIC、ACC_SUPER 為真,而 ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM六個標誌位都應該為假,所以它的 acess_flags 應該為 0x0001|0x0020=0x0021,如圖,偏移地址:000000D9
類索引、父類索引與介面索引集合
這三個索引用來確定這個類的繼承關係:
- 類索引:u2 型別的資料,用於確定類的全限定名。本例子中為 0x0003,指向常量池中第3項;
- 父類索引:u2 型別的資料,用於確定父類的全限定名。本例子中為 0x0004,指向常量池中第4項;
- 介面索引集合:一組 u2 型別的資料的集合,用於確定實現的介面(對於介面來說就是 extend 的介面)。第一項為介面索引計數器,u2 型別的資料,用於表示索引集合的容量。本例子中為 0x0000,說明沒有實現介面。
欄位表集合
欄位表(field_info)用於描述介面或者類中宣告的變數,包括類級變數和例項級變數,但不包括**方法內部宣告的區域性變數,**它不會列出從父類和超類繼承而來的欄位,但有可能列出原本 Java 程式碼中不存在的欄位,如在內部類中為了保持對外部類的訪問性所新增指向外部類例項的欄位。並且在 Java 語言中欄位是無法過載的,但對於位元組碼來說,欄位的描述符不同,欄位的重名是可行的。
欄位表的結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
其中欄位修飾符放在 access_flags 專案中,與類中 access_flags 類似,都是 u2 資料型別,可以設定的標誌位和含義:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 欄位是否為public |
ACC_PRIVATE | 0x0002 | 欄位是否為private |
ACC_PROTECTED | 0x0004 | 欄位是否為protected |
ACC_STATIC | 0x0008 | 欄位是否為static |
ACC_FINAL | 0x0010 | 欄位是否為final |
ACC_VOLATILE | 0x0040 | 欄位是否為volatile |
ACC_TRANSTENT | 0x0080 | 欄位是否為transient |
ACC_SYNCHETIC | 0x1000 | 欄位是否為由編譯器自動產生 |
ACC_ENUM | 0x4000 | 欄位是否為enum |
ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 最多隻能三選一,ACC_FINAL、ACC_VOLATILE不能同時選擇。介面中欄位必須有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL標誌。
access_flags 之後是 name_index 和 descriptor_index 。它們是對常量池的引用,分別代表著欄位的簡單名稱以及欄位方法和方法的描述符。
描述符是用來描述欄位的資料型別、方法的引數列表和返回值。描述符規則:
標誌符 | 含義 |
---|---|
B | 基本資料型別 byte |
C | 基本資料型別 char |
D | 基本資料型別 double |
F | 基本資料型別 float |
I | 基本資料型別 int |
J | 基本資料型別 long |
S | 基本資料型別 short |
Z | 基本資料型別 boolean |
V | 基本資料型別 void |
L | 物件型別,如 Ljava/lang/Object |
對於陣列型別,每一維度將使用一個前置的“[”字元來描述.如一個定義為"java.lang.Stirng[][]"型別的二維陣列,將被記錄為:“[[Ljava/lang/Stirng;”,一個整型陣列“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”。
- 例中第一個 u2 資料是容量計數器 fields_count,值為 0x0001,說明這類只有一個欄位表資料
- 接下來是 acesss_flags 標誌,值為 0x0002,代表修飾符的 ACC_PRIVATE 標誌為真
- 接著是修飾字段名稱的 name_index ,值為 0x0005,常量池中的第五個常量,也就是 CONSTANT_Utf8_info 型別的字串,值為 “m”
- 代表欄位描述符的 descriptor_index 的值為 0x0006,指向常量池的字串"I"
- 所以原始碼定義的欄位為:
private int m;
在 descriptor_index 之後跟隨著一個屬性表集合(attribute_info)用於儲存一些額外的資訊,欄位都可以在屬性表中描述零至多項的額外資訊。對於本例中的欄位 m,它的屬性計數器為 0,沒有需要額外描述的資訊,如果欄位 m 的宣告改為 final static int m = 1;
,那就可能會存在一項 ConstantValue 的屬性,其值指向常量 1。
方法表集合
和欄位表類似,方法表的結構依次包括訪問標誌(acess_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)幾項。
volatile 和 transient 不能修飾方法,但 synchronized、native、strictfp 和 abstract 關鍵字可以修飾方法,所以方法表的標誌位:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否為public |
ACC_PRIVATE | 0x0002 | 方法是否為private |
ACC_PROTECTED | 0x0004 | 方法是否為protected |
ACC_STATIC | 0x0008 | 方法是否為static |
ACC_FINAL | 0x0010 | 方法是否為final |
ACC_SYHCHRONRIZED | 0x0020 | 方法是否為synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是有編譯器產生的方法 |
ACC_VARARGS | 0x0080 | 方法是否接受引數 |
ACC_NATIVE | 0x0100 | 方法是否為native |
ACC_ABSTRACT | 0x0400 | 方法是否為abstract |
ACC_STRICTFP | 0x0800 | 方法是否為strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是有編譯器自動產生的 |
在例子中:
第一個 u2 型別的資料(計數器)為 0x0002,代表集合有兩個方法(分別為編譯器新增的例項構造器<init>
和原始碼中的 inc()
)。
- 第一個方法的訪問標誌為 0x0001,即只有 ACC_PUBLIC 為真
- 名稱索引值為 0x0007,常量池第七項,即 “”
- 描述符索引值為 0x0008,即 “()V”
- 屬性表計數器為 0x0001,表示此方法屬性表集合有一項屬性,屬性名稱索引為 0x0009,對應常量池第九項 “Code”,說明此屬性是方法的位元組碼描述。
第二個方法,inc()
方法:
與欄位表集合對應的,如果父類方法沒有被重寫(Override),方法表集合中就不會出現來自父類的方法資訊。但同樣會出現由編譯器自動新增的方法,最典型的是類構造器 <clinit>
方法和例項構造器 <init>
方法。
Java 語言中無法通過返回值不同來對一個已有方法進行過載的,但在 Class 檔案結構中,只要描述符不同的兩個方法也可以共存。也就是說,返回值不同,也可以 共存於同一個 Class 檔案中。
屬性表集合
在 Class 檔案、欄位表、方法表都可以攜帶自己的屬性表集合,以描述某些場景專有的資訊。屬性表集合不要求各個屬性表的嚴格順序,只要不要已有屬性名重複,任何人實現的編譯器都可以向屬性表寫入自己定義的屬性資訊,JVM 執行時會忽略不認識的屬性。
虛擬機器規範預定義的屬性:
屬性名稱 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java程式碼編譯成的位元組碼指令 |
ConstantValue | 欄位表 | final關鍵字定義的常量池 |
Deprecated | 類,方法,欄位表 | 被宣告為deprecated的方法和欄位 |
Exceptions | 方法表 | 方法丟擲的異常 |
EnclosingMethod | 類檔案 | 僅當一個類為區域性類或者匿名類是才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法 |
InnerClass | 類檔案 | 內部類列表 |
LineNumberTable | Code屬性 | Java原始碼的行號與位元組碼指令的對應關係 |
LocalVariableTable | Code屬性 | 方法的區域性變數描述 |
StackMapTable | Code屬性 | JDK1.6中新增的屬性,供新的型別檢查驗證器(Type Checker)檢查和處理目標方法的區域性變數和運算元棧所需要的類是否匹配 |
Signature | 類,方法表,欄位表 | 用於支援泛型情況下的方法簽名 |
SourceFile | 類檔案 | 記錄原始檔名稱 |
SourceDebugExtension | 類檔案 | 用於儲存額外的除錯資訊 |
Synthetic | 類,方法表,欄位表 | 標誌方法或欄位為編譯器自動生成的 |
LocalVariableTypeTable | 類 | 使用特徵簽名代替描述符,是為了引入泛型語法之後能描述泛型引數化型別而新增 |
RuntimeVisibleAnnotations | 類,方法表,欄位表 | 為動態註解提供支援 |
RuntimeInvisibleAnnotations | 表,方法表,欄位表 | 用於指明哪些註解是執行時不可見的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用與RuntimeVisibleAnnotations屬性類似,只不過作用物件為方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用與RuntimeInvisibleAnnotations屬性類似,作用物件為方法引數 |
AnnotationDefault | 方法表 | 用於記錄註解類元素的預設值 |
BootstrapMethods | 類檔案 | 用於儲存invokeddynamic指令引用的引導方式限定符 |
屬性表的結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
Code 屬性
Java 程式方法體中的程式碼經過 Javac 編譯器處理後,變為位元組碼指令儲存在 Code 屬性內。並非所有方法表都存在這個屬性,比如介面或抽象類中的方法。
Code 屬性表結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
本例中的第一個方法 <init>
,對應位元組碼:
其中
- 0x0009 代表屬性名稱"Code"
- 0x0000002f 是屬性值長度,屬性值長度固定為整個屬性表長度減去 6 個位元組。
- 0x0001 代表運算元棧(Operad Stacks)深度的最大值
- 0x0001 代表區域性變量表所需的儲存空間。單位是 Slot,Slot 是虛擬機器為區域性變數分配記憶體所使用的最小單位。方法引數(包括例項方法中的"this")、顯式異常處理器的引數(Exception Handler Parameter,try-catch語句中 catch 塊中所定義的異常)、方法體中定義的區域性變數需要區域性變量表來存放。
- 0x00000005 儲存 Java 源程式編譯後的位元組碼指令長度。雖然是 4 位元組的長度值,理論上最大值可以達到 2^32 -1,但是虛擬機器規範限制了一個方法不允許超過 65535 條位元組碼指令。
- 0x2ab70001b1 是位元組碼指令,每個指令就是一個 u1 型別的單位元組:
- 2a 對應指令為 aload_0,即將第 0 個 Slot 中為 reference 型別的本地變數推送到運算元棧頂
- b7 對應指令 invokespecial,以棧頂的 reference 型別的資料所指向物件作為方法接收者,呼叫此物件的例項構造器方法、private 方法或者父類方法。這個方法有一個 u2 型別的引數說明呼叫什麼方法,它指向常量池中的一個 CONSTANT_Methodref_info 型別常量,即此方法的符號引用。
- 0001,這是 invokespecial 的引數,常量池 0x0001對應的常量為例項構造器
<init>
方法的符號引用 - b1 對應指令是 return,返回此方法,並且返回值為 void 。
通過 javap 得到另一個方法:
其中 args_size = 1 ,因為例項方法,都可以通過 “this” 關鍵字訪問到此方法所屬的物件。因此在例項方法的區域性變量表中至少會存在一個指向當前物件例項的區域性變數,區域性變量表中也會預留出第一個 Slot 位來存放物件例項的引用,方法引數值也會從 1 開始計算,這個處理只對例項方法有效。
在位元組碼指令之後是這個方法的顯示異常處理表集合,異常表對於 Code 屬性並不是必須存在的。
異常表格式:
型別 | 名稱 | 數量 |
---|---|---|
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
這四個欄位,如果當位元組碼在第 start_pc 行到 end_pc行(不含第 end_pc 行)出現型別為 catch_type 或者其子類的異常,則轉到第 handler_pc 行繼續處理,當 catch_type 的值為 0 ,代表任意異常情況都需要轉到 handler_pc 處來進行處理。
編譯器通過異常表來實現 Java 異常以及 finally 處理機制。
Exceptions 屬性
Exceptions 屬性列舉出方法中基本丟擲的受查異常(Checked Exception),也就是方法描述時 throws 關鍵字後面列舉的異常,和 Code 屬性裡的異常表不同。
其屬性表結構如下:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_lrngth | 1 |
u2 | attribute_of_exception | 1 |
u2 | exception_index_tsble | number_of_exceptions |
Exceptions 屬性中的 number_of_exceptions 項表示方法可能丟擲 number_of_exceptions 種受查異常,每一種受查異常使用一個 exception_index_table 項表示,exception_index_table 是一個指向常量池中 CONSTANT_Class_info 型常量的索引,代表了該受查異常的型別。
LineNumberTable 屬性
LineNumberTable 屬性用於描述 Java 原始碼行號與位元組碼行號(位元組碼偏移量)的對應關係。它預設會生成在 Class 檔案中,可以在 Javac 中通過 -g:none 或 -g:lines 選項來取消或生成這項資訊。如果沒有這個屬性,執行時拋異常不會顯示出錯的行號,在程式碼除錯時無法按照原始碼行來設定斷點。
結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
LocalVariableTable 屬性
它是用於描述棧幀中區域性變量表中的變數與 Java 原始碼中定義的變數之間的關係,它預設會生成在 Class 檔案中,可以在 Javac 中通過 -g:none 或 -g:vars 選項來取消或生成這項資訊。如果沒有這個屬性,所有的引數名稱都會丟失,取之以 arg0、arg1 這樣的佔位符來替代。
LocalVariableTable 結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
其中 local_variable_info 代表一個棧幀與原始碼中的區域性變數的關聯,結構:
型別 | 名稱 | 數量 | 說明 |
---|---|---|---|
u2 | start_pc | 1 | 區域性變數的生命週期開始的位元組碼偏移量 |
u2 | length | 1 | 區域性變數作用範圍覆蓋的長度 |
u2 | name_index | 1 | 指向常量池中 CONSTANT_Utf8_info 型別常量的索引,區域性變數名稱 |
u2 | descriptor_index | 1 | 指向常量池中 CONSTANT_Utf8_info 型別常量的索引,區域性變數描述符 |
u2 | index | 1 | 區域性變數在棧幀區域性變量表中 Slot 的位置,如果這個變數的資料型別為 64 位型別(long或double),它佔用的 Slot 為 index 和 index+1 這 2 個位置 |
SourceFile 屬性
SourceFile 屬性用於記錄生成這個 Class 檔案的原始碼檔名稱。可以使用 Javac 的 -g:none 和 -g:source 來關閉或生成。對於大多數類來說,類名和檔名相同,但有些例外情況(如內部類)例外。如果不生成這項屬性,當丟擲異常,堆疊將不會顯示出錯程式碼所屬的檔名。
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
sourcefile_index 資料項指向常量池中 CONSTANT_Utf8_info 型常量的索引,常量值是原始碼檔案的檔名。
ConstantValue 屬性
ConstantValue 屬性的作用是通知虛擬機器自動為靜態變數賦值。
只有被 static 關鍵字修飾的變數才可以用這個屬性。對於非 static 型別的變數的賦值是在例項構造器 <init>
方法中進行的。而對於類變數有兩種方式:在類構造器方法中或者使用 ConstantValue 屬性。
目前 Sun Javac 編譯器的選擇是:
- 同時使用 final 和 static 修飾的變數,並且為基本資料型別或 java.lang.String 型別使用 ConstantValue 屬性初始化
- 如果沒有 final 修飾,或並非基本資料型別,則選擇在
<clinit>
方法中初始化
虛擬機器規範中並沒有強制要求要求 ConstantValue 屬性的欄位必須設定 ACC_FINAL 標誌,只是必須設定 ACC_STATIC 標誌而已。對 final 關鍵字的要求是 Javac 編譯器自己加入的限制。
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
ConstantValue 屬性是一個定長屬性,其 attribute_length 資料項值必須固定為 2。constantvalue_index 代表常量池的一個字面量引用。
InnerClasses 屬性
InnerClasses 屬性用於記錄內部類與宿主類之間的關聯。
屬性結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_class | number_of_classes |
number_of_classes 記錄多少個內部類資訊,每一個內部類資訊由一個 inner_classes_info 表描述,其結構:
型別 | 名稱 | 數量 | 作用 |
---|---|---|---|
u2 | inner_classes_info_index | 1 | 指向常量池 CONSTANT_Utf8_info 型別常量的索引,內部類的符號引用 |
u2 | outer_classes_info_index | 1 | 指向常量池 CONSTANT_Utf8_info 型別常量的索引,宿主類的符號引用 |
u2 | inner_name_index | 1 | 指向常量池 CONSTANT_Utf8_info 型別常量的索引,內部類的名稱,如果是匿名內部類,則為 0 |
u2 | inner_class_access_flags | 1 | 內部類的訪問標誌 |
inner_class_access_flags 標誌:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 內部類是否為public |
ACC_PRIVATE | 0x0002 | 內部類是否為private |
ACC_PROTECTED | 0x0004 | 內部類是否為protected |
ACC_STATIC | 0x0008 | 內部類是否為static |
ACC_FINAL | 0x0010 | 內部類是否為final |
ACC_INTERFACE | 0x0020 | 內部類是否為介面 |
ACC_ABSTRACT | 0x0400 | 內部類是否為抽象類 |
ACC_SYNCHETIC | 0x1000 | 欄位是否為由編譯器自動產生 |
ACC_ANNOTATION | 0x2000 | 內部類是否為註解 |
ACC_ENUM | 0x4000 | 內部類是否為列舉 |
Deprecated 和 Synthetic 屬性
這兩個都屬於標誌型別的布林屬性。
Deprecated 表示某個類、欄位或方法,已經被程式作者定義為不再推薦使用,可以在程式碼通過 @deprecated
標識。
Synthetic 表示欄位或方法不是由Java原始碼直接產生,而是編譯器自行新增的,唯一例外是例項構造器 <init>
和類構造器 <clinit>
。
Deprecated 和 Synthetic 屬性結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
attribute_length 資料項值必須為 0x00000000,因為不需要屬性值設定
StackMapTable 屬性
這是一個複雜的變長屬性,位於Code屬性的屬性表中。這個屬性會在虛擬機器類載入的位元組碼驗證階段被新型別檢查驗證器使用,目的在於代替以前比較消耗效能的基於資料流分析的型別推導驗證器。
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_entries | 1 |
stack_map_frame | stack_map_frame entries | number_of_entries |
Signature 屬性
一個可選的定長屬性,在 JDK 1.5 釋出後增加的,任何類、介面、初始化方法或成員的泛型簽名如果包含了型別變數或引數化型別,則 Signature 屬性會為它記錄泛型簽名信息。這主要是因為 Java 的泛型採用的是擦除法實現的偽泛型,在位元組碼中泛型資訊編譯之後統統被擦除,在執行期無法將泛型型別與使用者定義的普通型別同等對待。通過 Signature 屬性,Java 的反射 API 能夠獲取泛型型別。
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | signature_index | 1 |
其中 signature_index 值必須是一個對常量池的有效索引。常量池在該索引處的項必須是 CONSTANT_Utf8_info 結構,表示類簽名、方法型別簽名或欄位型別簽名。
BootstrapMethods 屬性
在JDK 1.7 之後加入到 Class 檔案規範中。它是一個複雜的變長屬性,位於類檔案的屬性表中,用於儲存 invokedynamic
指令引用的引導方法限定符。
位元組碼指令簡介
JVM 的指令由一個位元組長度、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其後的零至多個代表此操作所需引數(稱為運算元,Operands)而構成。由於Java虛擬機器採用面向運算元棧而不是暫存器的架構,所以大多數的指令都不包含運算元,只有一個操作碼。
載入和儲存指令
載入和儲存指令用於將資料在棧幀中的區域性變量表和運算元棧之間來回傳輸,指令包括:
iload/iload
等(載入區域性變數到操作棧)istore/istore
等(從運算元棧儲存到區域性變量表)bipush/sipush/ldc/iconst_<i>
(載入常量到運算元棧)wide
(擴充區域性變量表訪問索引)
其中部分指令(如 iload_<n>
),代表一組指令(如 iload_0、iload_1、iload_2、iload_3
),它們都是帶有一個運算元的通用指令,它們省略了顯式的運算元,如 iload_0
與運算元為 0 的 iload
指令語義完全一致。
###運算指令
運算或算術指令用於對兩個運算元棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。沒有直接支援 byte、short、char 和 boolean 型別的算術指令而採用 int 代替。
- 加減乘除:
iadd/isub/imul/idiv
- 求餘:
irem
- 取反:
ineg
- 位移:
ishl/ishr
- 按位或:
ior
- 按位與:
iand
- 按位異或:
ixor
- 區域性變數自增:
iinc
- 比較:
dcmpg/dcmpl/fcmpg/fcmpl/lcmp
型別轉換指令
型別轉換指令可以將兩種不同的數值型別進行互相轉換,這些轉換操作一般用於實現使用者程式碼中的顯式型別轉換操作。JVM 直接支援寬化型別轉換(Widening Numeric Conversions,小範圍向大範圍的安全轉換):
- int 到 long、float 或 double 型別
- long 到 float、double 型別
- float 到 double 型別
處理窄化型別轉換(Narrowing Numberic Conversions)時,必須顯式使用轉換指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f
。窄化型別轉換可能會導致轉換結果產生不同的正負號、不同的數量級的情況,可能會導致精度丟失。
物件建立與訪問指令
雖然類例項和陣列都是物件,但是虛擬機器建立類物件和陣列的指令是不同的。物件建立後,就可以通過物件訪問指令獲取物件例項或者陣列例項中的欄位或陣列元素,指令如下:
- 建立類例項的指令:
new
; - 建立陣列的指令:
newarray、anewarray、multianewarray;
- 訪問類欄位和例項欄位的指令:
getfield、putfield、getstatic、putstatic
; - 把一個數組元素載入到運算元棧的指令:
baload、caload、saload、iaload、laload、faload、daload、aaload
; - 將一個運算元棧的值儲存到陣列元素中的指令:
bastore、castore、sastore、iastore、fastore、dastore、aastore
; - 取陣列長度的指令:
arraylength
; - 檢查類例項型別的指令:
instanceof、checkcast
;
運算元棧管理指令
就像操作一個普通的棧一樣,Java 虛擬機器提供了一些用於直接操作運算元棧的指令,包括:
- 將運算元棧的棧頂一個或兩個元素出棧:
pop、pop2
; - 複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:
dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
; - 將棧頂最頂端的兩個數值互換:
swap
;
控制轉移指令
控制轉移指令可以讓 JVM 有條件或無條件的從指定的位置指令而不是控制轉移指令的下一條指令繼續執行,可以理解為控制轉移指令改變了 PC 暫存器的值。指令如下:
- 條件分支:
ifeq、iflt、ifle、ifgt、ifge、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、、if_icmpge、if_acmpeq和if_acmpne
; - 複合條件分支:
tableswitch、lookupswitch
; - 無條件分支:
goto、goto_w、jsr、jsr_w、ret
;
方法呼叫和返回指令
這裡僅僅列出 5 條用於方法呼叫的指令:
invokevirtual
指令用於呼叫物件的例項方法,根據物件的實際型別進行分派(虛方法分派),這也是 Java 語言中最常見的方法分派方式;invokeinterface
指令用於呼叫介面方法,它會在執行時搜尋一個實現了這個介面方法的物件,找出適合的方法進行呼叫;invokespecial
指令用於呼叫一些需要特殊處理的例項方法,包括例項初始化方法、私有方法和父類方法;invokestatic
指令用於呼叫類方法(static方法);invokedynamic
指令用於在執行時動態解析出呼叫點限定符索引用的方法,並執行方法,前面 4 條指令的分派邏輯都固化在 Java 虛擬機器內部,而 invokedynamic 指令的分派邏輯是由使用者所設定的引導方法決定的;
方法呼叫指令與型別無關,但是方法返回指令是根據返回值的型別區分的,包括 ireturn、lreturn、freturn、dreturn 和 areturn
,另外還有一個 return
指令供宣告為 void 的方法、例項初始化方法以及類和介面的類初始化方法使用。
異常處理指令
在 Java 程式中顯式丟擲異常的操作(throw 語句)都是由 athrow 指令來實現的,除了用 throw 語句顯式丟擲異常外,Java 虛擬機器規範還規定了許多執行時異常會在其他 Java 虛擬機器指令檢測到異常狀況時自動丟擲。
而在 Java 虛擬機器中,處理異常(catch語句)不是由位元組碼指令來完成的,而是採用異常表來完成的。
同步指令
Java 虛擬機器可以支援方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支援的。
方法級的同步是隱式的,即不需要通過位元組碼指令來控制,它實現在方法呼叫和返回操作中。虛擬機器可以從方法常量池的方法表結構中的 ACC_SYNCHRONIZED 訪問標誌得知一個方法是否宣告為同步方法。當方法呼叫時,呼叫指令就會去檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定了,如果設定,執行執行緒就要求持有管程。在方法執行期間,執行執行緒持有了管程,其他任何執行緒都無法再獲取到同一個管程。如果一個方法在執行期間發生了異常,並在方法中無法處理次異常,那麼這個同步方法所持有的管程將在異常丟擲後自動釋放。
同步一段指令集序列通常是由 Java 語言中的 synchronized 語句塊表示的,Java 虛擬機器的指令集中有 monitorenter
和monitorexit
指令來支援 synchronized 關鍵字的語義。正確實現 synchronized 關鍵字需要 Javac 編譯器和 Java 虛擬機器兩者共同協作。編譯器必須保證每個 monitorenter
指令都有對應的 monitorexit
指令。
公有設計和私有設計
JVM 規範描述了共同程式儲存格式:Class 檔案格式以及位元組碼指令集。這些內容與硬體、作業系統以及具體的 JVM 實現之間是完全獨立的,虛擬機器實現者更願意把它們看做是程式在各種 Java 平臺之間互相安全的互動的手段。
Java 虛擬機器的實現必須能夠讀取 Class 檔案並精確實現包含在其中的 Java 虛擬機器程式碼的含義。但一個優秀的虛擬機器實現,在滿足虛擬機器規範的約束下具體實現做出修改和優化也是完全可行,甚至是被鼓勵的。虛擬機器後臺如何處理 Class 檔案並不關心,只要外部介面的表現與規範描述的一致即可。
虛擬機器實現的方式主要有兩種:
- 將輸入的Java虛擬機器程式碼在載入或執行時翻譯成另外一種虛擬機器的指令集
- 將輸入的Java虛擬機器程式碼在載入或執行時翻譯宿主機 CPU 的本地指令集(即 JIT 程式碼生成技術)
Class 檔案結構的發展
Class 檔案結構一直比較穩定,主要的改進集中向訪問標誌、屬性表這些可擴充套件的資料結構中新增內容。Class 檔案格式所具備的平臺中立、緊湊、穩定和可擴充套件的特點,是 Java 技術體系實現平臺無關、語言無關兩項特性的重要支柱。
小結
本章詳細講解了Class檔案結構的各個部分,通過一個例項演示了Class的資料是如何儲存和訪問的,後面的章節將以動態的、執行時的角度去看看位元組碼在虛擬機器執行引擎是怎樣被解析執行的。
參考資料
- 周志明. 深入理解Java虛擬機器 : JVM高階特性與最佳實踐 : Understanding the JVM : advanced features and best practices[M]. 機械工業出版社, 2013.