JVM學習筆記(二)——Class文件結構
Class文件是Java程序跨平台的保證,正是由於有了Class文件架起源碼和機器碼之間的中間橋梁,JVM虛擬機才可以在各種平台上按照統一的規範標準加載Java代碼。
作為“寫給虛擬機看的”Java代碼,Class文件結構必須設計得足夠完善,同時由於Java虛擬機規範並不隻針對Java,Class文件又不能引入過多細節。本篇博客我們就來介紹下Class文件的結構。
一個Class文件對應一個Java Class,所以一個Class文件記錄著一個類的全部信息,JVM通過Class文件將對應的類加載入內存。
Class文件的結構主要分為以下幾部分:
- 魔數
- 常量池
- 訪問標識
- 類索引、父類索引、接口索引
- 字段表集合
- 方法表集合
- 索引表集合
1 魔數
每個Class文件的頭4個字節成為魔數(Magic Number),它的唯一作用就是確定這個文件是否能作為一個Class文件被接受。很多文件都以魔數進行類型識別,如gif、jpeg等圖片文件。之所以使用魔數而不是擴展名是處於安全考慮,文件擴展名可以所以改動。Class文件的魔數是0xCAFEBABE。
緊接著魔數的4個字節存儲的是Class文件的版本號,5、6字節為次版本號,7、8字節為主版本號。不同版本的虛擬機可以接受不同版本的class文件,所以虛擬機通過主次版本號判斷是否可以加載目標class文件。
2 常量池
常量池可以看做是Class文件的資源倉庫,也是Class文件中占用空間最大的部分。常量池主要存放兩大類常量:字麵量、符號引用。
字麵量比較接近Java語言層麵的常量,如文本字符串、生命為final的常量等。
符號引用屬於編譯範疇中的概念,主要包括三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
Java語言不同於C、C++等語言在編譯階段即進行鏈接,相應的鏈接都放到了運行時階段。所以Class文件中不可能包含各個方法、字段在內存中的布局。Java虛擬機在運行階段加載類時,將符號引用轉換成真正的內存入口地址,對應類才算可以工作。
常量池中的每一項代表一個常量,JDK目前共有14中類型的常量,而每一個常量又有自己的內部結構。類或接口符號索引是其中較為簡單的一項,接下來以類索引為例做簡單介紹。類符號索引對應的類型為CONSTANT_Class_info,其結構如下:
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
tag是標誌位,表明類型。CONSTANT_Class_info的tag為7。name_index是一個索引值,它指向常量池中一個CONSTANT_Utf8_info類型常量,此常量代表了這個類的全限定名。
CONSTANT_Utf8_info的結構如下所示:
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
bytes字段的內容就是類的全限定名。
3 訪問標誌
常量池之後的兩個字節代表訪問標誌(accss_flags),用於識別類或接口的層次訪問信息:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為public |
ACC_FINAL | 0x0010 | 是否被聲明為final |
ACC_SUPER | 0x0020 | 是否允許使用invokespecial字節碼指令的新語義 |
ACC_INTERFACE | 0x0200 | 是否為接口 |
ACCS_ABSTRACT | 0x0400 | 是否為abstract類型 |
ACC_SYNTHETIC | 0x1000 | 標示該類並非由用戶代碼產生 |
ACC_ANNOTATION | 0x2000 | 標示這是一個注解 |
ACC_ENUM | 0x4000 | 標示這是一個枚舉 |
4 類索引、父類索引與接口索引集合
類索引(this_class)和父類索引(super_class)都是一個u2類型的數據,而接口索引集合(interfaces)是一組u2類型的數據集合。Class文件中的這三項決定了類的繼承關係。
類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個CONSTANT_Class_info類描述符常量,通過CONSTANT_Class_info類型的常量索引值可以找到定義在CONSTAN_Utf8_info類型的常量中的類全限定名。
對於接口索引集合,入口的第一項u2類型的數據為接口計數器(interfaces_count)表示索引表的容量。每個接口的同樣由一個u2類型數據指向一個CONSTANT_Class_info。
5 字段表集合
字段表(field_info)用於描述接口或者類中聲明的變量。字段(field)包括類級變量和實例級變量,但不包括定義在方法內部的局部變量。每個字段的結構如下圖所示:
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attribute_count |
5.1 訪問標識
標誌名稱 | 含義 |
---|---|
ACC_PUBLIC | 是否為public |
ACC_PRIVATE | 是否為private |
ACC_PROTECTED | 是否為protected |
ACC_STATIC | 是否為static |
ACC_FINAL | 是否為final |
ACC_VOLATILE | 是否為volatile |
ACC_TRANSIENT | 是否為transient |
ACC_SYNTHETIC | 是否為編譯器自動產生 |
ACC_ENUM | 是否為enum |
字段的訪問標識access_flags與類訪問標識類似。
5.2 name_index
name_index標識字段的簡單名稱。簡單名稱和全限定名的區別在於:全限定名是類的全路徑名,如org/fenixsoft/clazz/TestClass,隻是把類全名中的"."替換成“/”而已。簡單名稱指的是沒有類型和參數修飾的方法或者字段名稱,如一個類中含有一個字段"m",則其簡單名稱為"m"。
5.3 descriptor_index
descriptor_index為字段或方法的描述符。描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。基本數據類型以及代表無返回值的void以及對象類型均由一個大寫字符來代替:
標誌字段 | 含義 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 對象類型,如Ljava/lang/object |
對於數組類型,每一個維度用一個"["來描述,比如定義一個“java.lang.String[][]”類型的二維數組,將被記錄為“[[Ljava/lang/string”。
方法描述符按照先參數列表後返回值的順序描述,參數列表按照參數順序放在一組"()"之內。如方法int indexOf(char[] source, int sourceOffest, int sourceCount, char[] target, int targetOffest, int targetCount, int fromIndex)的描述符為"([CII[CIII)I"。
5.4 attributes_count attribute_info
在描述符之後還有數量為attributes_count的attribute_info,attribute_info描述字段的額外信息,但這些額外信息最終存放在屬性表中。如“final static int m = 123;”,那就可能會存在一項名稱為ConstantValue的屬性,其值指向常量123。
6 方法表集合
方法表和字段表結合幾乎一樣,理解了字段表,方法表就非常簡單了。
| 類型 | 名稱 | 數量 |
| -------------- | ---------------- | --------------- |
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attribute_count |
由於volatile和transient不能修飾方法,所以方法表的訪問標識中沒有了ACC_VOLATILE,ACC_TRANSIENT標識。但同時又增加了代表synchronized native strictfp abstract的ACC_SYNCHRONIZED ACC_NATIVE ACC_STRICTFP ACC_ABSTRACT。
需要說明的是,方法表集合中並不包含方法裏麵的代碼。方法代碼經過編譯後存放在方法屬性集合中的一個名為"Code"的屬性裏麵。例如某方法的屬性表計數器attributes_count為1,則表示方法的屬性表集合有一項屬性,屬性索引名稱為0x0009,對應常量為code,說明此屬性是方法的字節碼描述。
7 屬性表集合
Class文件、字段表、方法表都可以有自己的屬性表,Java7裏麵定義了21種屬性。
Code屬性
並非所有方法表都有Code屬性,比如接口和抽象類的方法就沒有。結構如下:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名的索引,對Code屬性而言恒為”Code” |
u4 | attribute_length | 1 | 屬性值長度,相當於整個屬性表長度長度減6(u2+u4) |
u2 | max_stack | 1 | 操作數棧深度最大值。JVM運行時根據此值分配棧楨的操作棧深度 |
u2 | max_locals | 1 | 局部變量表所需存儲空間,單位是Slot,double和long占用2個Slot、其他基本類型1Slot,Slot空間可以重用(變量作用域問題) |
u4 | code_length | 1 | 編譯後的字節碼長度,理論上最長2^32-1,實際上JVM規定一個方法不允許超過65535條字節碼指令 |
u1 | code | code_length | 代碼編譯後的字節碼 |
u2 | exception_table_length | 1 | 異常表長度 |
exception_info | exception_table | exception_table_length | 異常表,記錄字節碼在start_pc到end_pc行之間如果出現類型為catch_type或其子類的異常則跳轉到handler_pc行繼續處理 |
u2 | attibutes_count | 1 | 屬性表計數器 |
attribute_info | attributes | attibutes_count | 屬性額外描述,比如描述變量初始化值在常量池中的索引 |
字節碼值得注意的一個地方是,javac編譯時將this關鍵字作為一個普通方法參數由JVM調用時自動傳入。
Exceptions屬性
描述方法可能拋出的受檢異常。
LineNumberTable屬性
描述Java遠嗎行號與字節碼行號之間映射關係,也就是為什麼拋異常的時候可以顯示源碼哪一行拋出的。
LocalVariableTable屬性
描述棧楨中局部變量表與Java源碼中變量的關係,以保證編譯後的代碼被其他代碼調用時,IDE可以顯示參數名(否則被arg0、arg1之類的變量名代替)
SourceFile屬性
描述生成當前Class文件的源文件名稱,也是拋異常時可以顯示源文件名字的原因。但內部類不會生成這個屬性。
ConstantValue屬性
static關鍵字修飾的變量可以使用這個屬性。對於Sun javac編譯器,final static的變量采用ConstantValue屬性初始化,其他static變量在(類構造器)中初始化。
InnerClasses屬性
記錄內部類和宿主類的關聯。內部類和宿主類的Class文件都會有這個屬性。
Signature屬性
記錄泛型簽名信息。Java的泛型是使用擦除式實現的偽泛型,編譯後擦除泛型,這個屬性為了彌補此缺陷,方便反射API可以拿到泛型類型。
最後更新:2017-07-11 01:02:47