前言
要想深入的瞭解jvm,瞭解java編譯後的類檔案結構和位元組碼是很有必要的。雖然這部分內容(主要是class檔案的資料結構)比較枯燥,但是這也是最基礎的內容,是我們深入理解jvm的記憶體、類的載入等內容的基石。
Class類檔案結構
class檔案是一組以8位位元組為基礎的二進位制流,各個資料專案按照順序排列在Class檔案中,中間沒有任何分隔符。因此整個class檔案中儲存的內容幾乎全是程式執行時的必要資料。當遇到需要佔用8位以上位元組空間的資料項時,會按照高位在前的方式分割成若干個8位位元組儲存。
class檔案格式如下:
型別 | 名稱 | 數量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count - 1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
class檔案只有兩種偽資料結構:無符號數和表。可以看到每個表的前面都會有一XX count的,這是一個前置容量計數器,用來記錄對應型別的數量。
資料項
class檔案的資料項很多,這裡不展開一個個地講,主要介紹一些關鍵的。
- 常量池
- 欄位表
- 方法表
- 屬性表
常量池
常量池相信很多人都聽過,這裡的常量池指的是class檔案中的常量池。
常量池中主要儲存兩類型別:字面量和符號引用。
字面量:字面量指的是java語言層的常亮,如String s="123",那麼這個"123"就是常量。對於基本型別的封裝型別,範圍在-127-128之間的,也是常量。當然了,宣告為final的值,在整個程式執行過程中不可變,自然也是常量了。
符號引用:java中的符號引用主要包括以下3類常量:
- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
java是在虛擬機器載入class檔案的時候進行動態連線的,class檔案中不會儲存各方法欄位的最終記憶體佈局資訊,因此這些欄位、方法的符號引用不經過虛擬機器執行時轉換的話,無法得到真正的記憶體地址,也就無法被jvm使用。當虛擬機器執行時,需要從常量池獲得對應的符號引用,再在類建立或執行時解析,翻譯到具體的記憶體地址中。
常量池中的每一項常量都是一個表,這些表有一個共同的特點,每個表的第一位都是一個u1型別的標誌位,取值如下:
常量池中資料項型別 | 型別標誌 | 型別描述 |
---|---|---|
CONSTANT_Utf8 | 1 | UTF-8編碼的Unicode字串 |
CONSTANT_Integer | 3 | int型別字面值 |
CONSTANT_Float | 4 | float型別字面值 |
CONSTANT_Long | 5 | long型別字面值 |
CONSTANT_Double | 6 | double型別字面值 |
CONSTANT_Class | 7 | 對一個類或介面的符號引用 |
CONSTANT_String | 8 | String型別字面值 |
CONSTANT_Fieldref | 9 | 對一個欄位的符號引用 |
CONSTANT_Methodref | 10 | 對一個類中宣告的方法的符號引用 |
CONSTANT_InterfaceMethodref | 11 | 對一個介面中宣告的方法的符號引用 |
CONSTANT_NameAndType | 12 | 對一個欄位或方法的部分符號引用 |
訪問標誌
訪問標誌用來識別類或介面的訪問資訊,比如這個Class是類還是介面,是public還是private,是否被宣告為final等。具體標誌位及含義如下:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x00 01 | 是否為Public型別 |
ACC_FINAL | 0x00 10 | 是否被宣告為final,只有類可以設定 |
ACC_SUPER | 0x00 20 | 是否允許使用invokespecial位元組碼指令的新語義. |
ACC_INTERFACE | 0x02 00 | 標誌這是一個介面 |
ACC_ABSTRACT | 0x04 00 | 是否為abstract型別,對於介面或者抽象類來說,次標誌值為真,其他型別為假 |
ACC_SYNTHETIC | 0x10 00 | 標誌這個類並非由使用者程式碼產生 |
ACC_ANNOTATION | 0x20 00 | 標誌這是一個註解 |
ACC_ENUM | 0x40 00 | 標誌這是一個列舉 |
類索引、父類索引與介面索引集合
我們都知道,java中是單繼承多實現的,除了Object類之外每個類都有父類,因此它們是唯一的,而一個類可以實現多個介面,因此介面是不唯一的,用集合表示。類索引和父類索引都是用一個u2型別資料來表示的,而介面索引集合則是一組u2型別的資料表示。
類索引、父類索引和介面索引集合都按順序排在訪問標誌的後面。類索引和父類索引u2型別的索引各指向一個型別為CONSTANT_Class_info的類描述符常量,用來描述具體的類。對於介面索引第一項u2則為介面索引計數器,用來記錄實現了多少個介面,如果為0則後面不再佔用任何位元組。
欄位表集合
欄位表用於宣告類或介面中宣告的變數。欄位包括類變數和例項變數。在java中一個欄位是如何描述的,舉個的例子
public static final String num="13234";
可以看出來,首先是訪問範圍,是公有的還是私有的,或者受保護的,這個資訊決定了欄位是否堆特定範圍的類可見。
其次是一些關鍵字修飾的描述資訊,是例項變數還是類變數,是否可變,併發可見性,是否可被序列化等,這些關鍵字包括static、final 、volatile、transient等。
在後面便是欄位的資料型別(基本資料型別、陣列、物件)和名稱。
上述的這些修飾符都是用布林值來描述的,而資料型別和名稱都是不確定的,通常引用常量池的常量來描述。
欄位表結構如下:
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
欄位修飾符在access_flag下,access_flag的內容如下:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
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 |
name_index表示的是欄位的簡單名稱,像上面的簡單名稱就是“num”,descriptor_index 表示欄位和方法的描述符。
描述符號的作用是用來描述欄位的資料型別、方法的引數列表(數量、型別及順序)和返回值。根據描述符的規則:基本資料型別以及代表無返回值的void型別都用一個大寫的字元來表示,而物件型別則用字元L加物件的全限定名來描述,詳情如下:
標誌符 | 含義 |
---|---|
B | 基本資料型別byte |
C | 基本資料型別char |
D | 基本資料型別double |
F | 基本資料型別float |
I | 基本資料型別int |
J | 基本資料型別long |
S | 基本資料型別short |
Z | 基本資料型別boolean |
V | 基本資料型別void |
L | 物件型別 |
對於陣列型別,每一個維度用一個前置的“[”字元來描述,如定義個int[][]型別的二維陣列,記錄為:"[[I"。
用描述符來描述方法時,按照先引數列表後返回值的順序描述。引數裂變按照引數順序放在“()”內,如方法void login()描述符為“()V”,方法java.lang.String toString()的描述符為“()Ljava.lang.String”。
方法表集合
Class檔案儲存格式中對方法和欄位的描述完全一致,方法表的欄位結構和欄位表一樣,包括訪問標誌、名稱索引、描述符索引、屬性表集合幾項。這些資料的含義非常類似,在訪問標誌和屬性表集合有所區別。
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
voliate和transient不能修飾方法,所以訪問標誌沒有了ACC_VOLATILE標誌和ACC_TRANSIENT標誌。同樣,一些方法的關鍵字如:synchronized、native、strictfp、abstract等可以修飾方法,其標誌位取值如下:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x00 01 | 方法是否為public |
ACC_PRIVATE | 0x00 02 | 方法是否為private |
ACC_PROTECTED | 0x00 04 | 方法是否為protected |
ACC_STATIC | 0x00 08 | 方法是否為static |
ACC_FINAL | 0x00 10 | 方法是否為final |
ACC_SYHCHRONRIZED | 0x00 20 | 方法是否為synchronized |
ACC_BRIDGE | 0x00 40 | 方法是否是有編譯器產生的方法 |
ACC_VARARGS | 0x00 80 | 方法是否接受引數 |
ACC_NATIVE | 0x01 00 | 方法是否為native |
ACC_ABSTRACT | 0x04 00 | 方法是否為abstract |
ACC_STRICTFP | 0x08 00 | 方法是否為strictfp |
ACC_SYNTHETIC | 0x10 00 | 方法是否是有編譯器自動產生的 |
方法裡的程式碼
方法的定義可以通過訪問標誌、名稱和描述符索引來表述清除,那麼方法中的程式碼又在哪裡呢?我們之前提到了屬性表集合,方法裡的java程式碼經過編譯器編譯成位元組碼指令後,存放在方法的屬性表集合裡一個名為“Code”的屬性裡。
屬性表集合
我們從上面不止一次的看到了屬性表集合,不管是Class檔案、欄位表還是方法表中,都有屬性表集合(attribute_info)這一項,用於描述某些特定場景的專有資訊。
與class檔案其它資料專案嚴格要求順序長度不同,屬性表集合限制相對比較寬鬆,不要求各個屬性表具有嚴格順序,只要不與已有屬性名重複,任何人實現的編譯器均可向屬性表寫入自己的屬性,jvm執行時會自動忽略掉不認識的屬性。
java7中定義的屬性如下表:
屬性名稱 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java程式碼編譯成的位元組碼指令 |
ConstantValue | 欄位表 | final關鍵字定義的常量池 |
Deprecated | 類,方法,欄位表 | 被宣告為deprecated的方法和欄位 |
Exceptions | 方法表 | 方法丟擲的異常 |
EnclosingMethod | 類檔案 | 僅當一個類為區域性類或者匿名類是才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法 |
InnerClass | 類檔案 | 內部類列表 |
LineNumberTable | Code屬性 | Java原始碼的行號與位元組碼指令的對應關係 |
LocalVariableTable | Code屬性 | 方法的區域性便狼描述 |
StackMapTable | Code屬性 | JDK1.6中新增的屬性,供新的型別檢查檢驗器檢查和處理目標方法的區域性變數和運算元有所需要的類是否匹配 |
Signature | 類,方法表,欄位表 | 用於支援泛型情況下的方法簽名 |
SourceFile | 類檔案 | 記錄原始檔名稱 |
SourceDebugExtension | 類檔案 | 用於儲存額外的除錯資訊 |
Synthetic | 類,方法表,欄位表 | 標誌方法或欄位為編譯器自動生成的 |
LocalVariableTypeTable | 類 | 使用特徵簽名代替描述符,是為了引入泛型語法之後能描述泛型引數化型別而新增 |
RuntimeVisibleAnnotations | 類,方法表,欄位表 | 為動態註解提供支援 |
RuntimeInvisibleAnnotations | 表,方法表,欄位表 | 用於指明哪些註解是執行時不可見的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用與RuntimeVisibleAnnotations屬性類似,只不過作用物件為方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用與RuntimeInvisibleAnnotations屬性類似,作用物件哪個為方法引數 |
AnnotationDefault | 方法表 | 用於記錄註解類元素的預設值 |
BootstrapMethods | 類檔案 | 用於儲存invokeddynamic指令引用的引導方式限定符 |