之前介紹過Java編譯器如何將Java源碼編譯成字節碼class文件。
那麼最終的到的字節碼文件是怎樣的一個文件,內部結構又是如何?此文對字節碼class文件的內部結構進行初步探索,介紹其各個重要組成部分,對之後的Java虛擬機學習做好基礎。
下面展示了一個class文件的構成,其中u2、u4等表示類型,分別表示佔2、4個字節的數據,屬於class文件的基本類型。cp_info表示常量池類型,field表示成員變量類型,method表示類或接口的方法類型,attribute表示屬性類型。
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count - 1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interface_count;
u2 interfaces[interface_count];
u2 fields_count;
field info_fields[fields_count];
u2 methods_count;
method info_methods[methods_count];
u2 attributes_count;
attribute info_attributes[attributes_count];
}
1. magic
每個字節碼文件開頭都包含4個字節的magic number:0xCAFEBABE 用來快速校驗是否是Java Class文件。Cafe babe和Java一樣,命名的由來都和咖啡有關。
2. minor_version 和 major_version
這4個字節先後包含的是次版本號和主版本號。JVM在校驗完魔法數後緊接着就會檢查版本號是否在JVM支持的有效範圍,如果過高版本的字節碼文件在低版本的JVM上是無法執行的。
3. constant_pool_count 和 constant_pool[]
常量池,裏面存儲了符號引用和字表量,包含了類和接口名、字段名和描述符、方法名和描述符、字符串、final變量值等等,這些信息以列表的形式存儲在常量池列表中。constant_pool_count即常量池計數項,用於記錄常量池中的常量數量,計數的總和等於1+常量池列表的項數,這裏的1代表常量池表項,索引爲0。
值得一提的是,常量池是字節碼文件內部結構中與外部關聯最多的數據項,也是佔空間最大的項。但是class文件不保存各個方法字段的內存佈局的信息,內存的分配是在運行時完成的。當類加載器將類加載到內存時,常量池中指向類和非final靜態字段的符號引用會轉換成直接引用指向指定的內存地址。當初始化生成一個對象時,常量池中指向非靜態字段的符號引用會轉換成直接引用指向指定的內存地址。當執行某個方法時,方法對應的當前棧幀中的動態鏈接會將常量池中指向方法的符號引用轉換爲調用方法的直接引用。
4. access_flags
訪問標誌項用來描述這個文件類型的一些訪問標誌信息。區分這個文件定義的是類還是接口,類或接口包含了哪些修飾符,是抽象的還是公共的或是final的。
類型 | 值 | 作用 |
---|---|---|
ACC_PUBLIC | 0x0001 | 聲明爲public,可以從它的包外訪問 |
ACC_FINAL | 0x0010 | 聲明爲final,不允許有子類 |
ACC_SUPER | 0x0020 | 用invokespecial指令處理超類的調用 |
ACC_INTERFACE | 0x0200 | 表明是一個接口 |
ACC_ABSTRACT | 0x0400 | 聲明爲abstract,不能被實例化 |
5. this_class 和 super_class
this_class的值指向了常量池中類型爲CONSTANT_Class_info的一個類的常量,進一步可以查到this_class的全限定名,super_class同理。因爲Java只支持單繼承,因此父類只有一個,super_class的值爲0時說明這個class文件的類直接繼承了java.lang.Object,其他情況不允許爲0。
6. interface_count 和 interfaces[]
interface_count用來記錄interfaces的容量,u2類型的interfaces代表這個類實現的接口以及接口的父接口的常量池索引集合,指向常量池接口常量。如果這個類沒有實現任何一個接口interface_count的值爲0。
7. fields_count 和 info_fields[]
fields_count用來記錄類或接口中聲明的變量的個數,info_fields是field類型列表,用來描述類或接口中聲明的變量,以上的變量指的都是指成員變量(類變量和實例變量),不包括方法內部的局部變量(方法內部的局部變量在方法在執行的時候存儲在當前棧幀中的局部變量表中,也就是我們說的局部變量存儲在Java棧區)。
類型 | 名稱 | 說明 |
u2 | access_flags | 聲明成員變量時使用的訪問標誌 |
u2 | name_index | 成員變量的名稱索引 |
u2 | descriptor_index | 變成員量的描述符索引 |
u2 | attributes_count | 屬性列表中屬性的個數 |
attribute_info | attributes | 成員變量的屬性 |
成員變量訪問標誌包含:
ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED, ACC_STATIC,
ACC_FINAL, ACC_VOLATILE(併發可見性)
name_index和descriptor_index指向常量池中成員變量的名稱和描述符。成員變量的名稱並不是全限定名,而是沒有類型的簡單名稱。成員變量的描述符描述其類型,描述符的格式:基本數據類型以及void類型都用一個特定的大寫字符表示,而對象類型用字符L加對象的全限定名來表示,而數組類型,N維數組就在前面個N個[,例如:String[][]的描述符就是“[[Ljava/lang/String”。
attributes_count記錄成員變量的屬性個數,如果沒有則爲0。在這裏會出現的屬性有ConstantValue屬性,後面會介紹這個屬性。
需要說明info_fields中不包含從父類或父接口中繼承來的字段信息。
8. methods_count 和 info_methods[]
methods_count用來記錄這個類中的所有方法個數,info_methods是method類型數組,用來描述類或接口中聲明的方法。method類型的結構類似field。
類型 | 名稱 | 說明 |
u2 | access_flags | 聲明方法時使用的訪問標誌 |
u2 | name_index | 方法的名稱索引 |
u2 | descriptor_index | 方法的描述符索引 |
u2 | attributes_count | 屬性列表中屬性的個數 |
attribute_info | attributes | 方法的屬性 |
類或接口中方法的訪問標誌包含:
ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED,
ACC_ABSTRACT,
ACC_STATIC, ACC_FINAL, ACC_SYNCHRONIZED, ACC_NATIVE, ACC_STRICT
name_index和descriptor_index指向常量池中方法名稱和描述符。方法的名稱並不是全限定名,而是簡單名稱。方法的描述符描述了方法的參數類型列表和返回值類型,參數類型列表的排序必須嚴格按照順序。方法描述符的格式是“(參數1類型,參數2類型...)返回類型”例如,方法java.lang.String toString()的描述符爲“()Ljava/lang/String”(L是表示引用類型),方法 void init()方法的描述符爲“()V”。這裏要說明的是,在Java語法中是不允許存在除返回類型不同其他完全相同的兩個方法,這會導致編譯不通過,但是Class文件允許這樣的情況存在。
講到這裏,方法的訪問標誌、方法名稱以及方法的描述符(參數列表和返回類型)都各歸其位,那麼最主要的方法體中的代碼呢?好了,方法屬性隆重登場,方法體中的方法代碼經過編譯器編譯成一條條指令存放在方法的屬性類型中有個“Code”屬性中。其實在棧幀中,局部變量表和操作數棧所需的容量大小在編譯期就可以完全被確定下來,並保存在方法的Code屬性中。不要忘記,方法還能拋出異常,而對於可能拋出的異常信息就存儲在屬性中的Exception屬性中。
同feild一個methods列表中不包含父類或父接口中繼承來的方法。
9. attributes_count 和 info_attributes[]
上文在field和method中已經出現並提到過attribute屬性,這裏的info_attributes是在最外層的class文件中,例如,一些內部類和匿名類的信息就存儲在對應的屬性項中。下面會列舉一些Java預定義的屬性。先來看attribute的內部結構。
類型 | 名稱 | 說明 |
u2 | attribute_name_index | 常量池中屬性名稱的索引 |
u4 | attribute_lenght | 屬性數據的長度(以字節計算) |
u1 | info | 包含的屬性數據,長度爲attribute_lenght |
Java虛擬機規範規定,只要遵循一定規則,任何人都能向class文件中加入屬性,這裏不做深入探討。下面列舉一些Java預定義的屬性。
屬性名稱 | 說明 | 長度 |
Code | Java代碼編譯成的字節碼指令 | 可變 |
ConstantValue | 只有同時被final和static修飾的成員變量纔有ConstantValue屬性,且限於基本類型和String | 固定 |
Deprecated | 過時的類、方法或成員變量 | 固定 |
Exceptions | 方法可能拋出的異常 | 可變 |
InnerClasses | 內部類列表 | 可變 |
SourceFile | 源文件名稱 | 固定 |
Synthetic | 標識類,方法或成員變量是否是編譯器自動生成的 | 固定 |
RuntimeVisibleAnnotations | 運行時可見註解 | 可變 |
RuntimeInvisibleAnnotations | 運行時不可見註解 | 可變 |
BootstrapMethods | 用於保存invokedynamic指令引用的引導方法限定符 | 可變 |
LineNumberTable | 源碼行數與字節碼指令對應關係 | 可變 |
LocalVariableTable | 方法的局部變量描述 | 可變 |
StackMapTable | 檢查和處理目標方法的局部變量和操作數棧所需的類型是否匹配 | 可變 |
以上是列舉的部分屬性,和class文件中的其他項不同的是,屬性列表沒有嚴格的順序要求,屬性項的長度也可以是可變的,因此需要數據結構中的attribute_lenght以字節爲單位記錄屬性數據的總長度。下面列舉下幾個屬性的內部結構。
類型 | 名稱 | 說明 |
u2 | attribute_name_index | 常量池中屬性名稱的索引 |
u4 | attribute_length | 屬性數據的長度(以字節計算) |
u2 | max_stack | 方法的操作數棧的最大長度(以字節計算) |
u2 | max_locals | 局部變量所需存儲的空間長度(以Slot計算) |
u4 | code_length | 方法的字節碼流長度(以字節計算) |
u1 | code | 方法的字節碼指令的一系列字節流 |
u2 | exception_table_length | exception_table的中異常的個數 |
exception_info | exception_table | 異常表 |
u2 | attributes_count | Code的屬性個數 |
attribute_info | attributes | Code屬性(LineNumberTable, LoaclVaribaleTable和StackMapTable) |
類型 | 名稱 | 說明 |
u2 | attribute_name_index | 常量池中屬性名稱的索引 |
u4 | attribute_length | 屬性數據的長度(固定爲2個字節) |
u2 | constantvalue_index | 常量池中常量值的索引 |
類型 | 名稱 | 說明 |
u2 | attribute_name_index | 常量池中屬性名稱的索引 |
u4 | attribute_length | 屬性數據的長度(以字節計算) |
u2 | number_of_exceptions | throws關鍵字後面列舉的異常數量 |
u2 | exception_index_table | 常量池中throws關鍵字後面列舉的異常的索引 |
總結
到這裏字節碼class文件大致的內部結構就介紹完了。
Java不像C或者C++那樣編譯完成後就保存了類、方法和變量的內存佈局信息,字節碼文件只有符號引用,而沒有直接指向內存空間的引用,是屬於靜態文件,因此纔會有之後的Java虛擬機加載字節碼文件進行動態連接,通過解析翻譯將符號引用轉換成直接引用,這也是Java的魅力所在。
對於Java虛擬機的初學者來說,剛接觸這些內容可能感覺會比較枯燥,沒有太多感知,容易忘記。但是隨着之後的學習,當了解了類加載的過程,方法的執行和棧幀的結構,堆內存以及對象的初始化等一系列原理後,再回過頭看這些內容,我相信肯定會有和第一次學習時有不一樣的感受。瞭解字節碼文件內部結構是接下去學習JVM的基石,當掌握JVM這套編譯執行流程後,字節碼文件的結構也自然會熟記在心。