文章目錄
前言
在上一篇文章中我們一起來看了一下 Java 虛擬機的類加載過程,包括虛擬機加載、驗證、準備、解析和初始化 5 個大步驟,同時我們還討論了 Java 虛擬機加載類時採用的雙親委派模型思想。在這篇文章中我們來一起看一下 class 文件的結構,來進一步加深我們對虛擬機的類加載機制和類機制的理解。本文參考了 《深入理解 Java 虛擬機》一書。
在正式開始之前我們先複習一下一些基礎知識,我們知道一個類的全名格式爲 包名.類名
。比如 java.lang.Object
。那麼一個類的全限定名是什麼呢?就是就是將類全名中的 .
換成 /
。所以 java.lang.Object
類的全限定名爲 java/lang/Object
。類的簡單名就是這個類的名字,比如 java.lang.Object
類的簡單名爲 Object
。
相對於類名和類的全限定名來說,方法的描述符就複雜一點,方法的描述符的作用是描述一個方法的方法名、參數類型和方法返回值。對於基本數據類型和代表無返回值的 void
類型都用一個大寫字母表示。而複雜對象類型則用字母 L
加上對象所屬類的全限定名錶示。如下表:
標識字符 | 含義 |
---|---|
B | 基本類型 byte |
C | 基本類型 char |
D | 基本類型 double |
F | 基本類型 float |
I | 基本類型 int |
J | 基本類型 long |
S | 基本類型 short |
Z | 基本類型 boolean |
V | 特殊類型 void |
L | 對象類型,如 Ljava/lang/Object |
對於數組類型,每一維度使用一個前置 [
來描述,如定義一個 int[]
類型的數組將被表示爲 [I
。java.lang.String[][]
將被表示爲 [[Ljava/lang/String;
。
用描述符描述方法時,將參數列表描述放在前面,返回值描述放在後面。比如方法 int getInt()
的描述爲 ()I
。方法 void setA()
的描述爲 ()V
。方法 int binarySearch(int[] arr, int goal)
的方法描述爲 ([II)I
。我們在下面解析 .class
文件中會在用到這裏介紹的知識點。
解析 .class 文件
我們都知道一個 Java 類(.java
)文件在被 Java 編譯器(javac
) 編譯過後,如果語法沒有錯誤,則會生成一個對應的 .class
文件,這個 .class
文件是一個二進制文件,用一定的格式保存了我們書寫的類的所有信息。不像 xml
和 json
這些帶有標誌的語言一樣,.class
文件是一個純二進制文件,其中的數據是緊湊並且沒有任何分隔符的,因此 .class
文件中的數據的順序和含義是具有非常嚴格的規定的,.class
文件中的數據格式可以用以下表格描述:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u4 | magic | 1 | 文件魔數 |
u2 | minor_version | 1 | 次版本號 |
u2 | major_version | 1 | 主版本號 |
u2 | constant_pool_count | 1 | 常量池中的常量數 + 1 |
cp_info | constant_pool | constant_pool_count - 1 | 常量池信息 |
u2 | access_flag | 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 | attributes_count | 1 | 類的其他屬性信息數量 |
attribute_info | attributes | attributes_count | 類的其他屬性信息 |
其中,u
代表無符號整數類型,u1
則代表 1 個字節的無符號整數類型,u2
代表 2 個字節的無符號整數類型,u4
代表 4 個字節的無符號整數類型,而其他的類型(cp_info
, field_info
, method_info
, attribute_info
)等則是 .class
文件自定義的數據結構(表)。有了這個基礎之後,我們來通過例子來理解上面表格的數據含義,新建一個 ClassContent
類:
public class ClassContent {
public static final String STR = "s";
public static final int A = 1;
public int getInt() {
int x = 1;
return x;
}
public static void main(String[] args) {
System.out.println("hello");
}
}
非常簡單的一段代碼,沒有什麼特殊含義,我們編譯這個類,可以得到對應的類文件(ClassContent.class
),
用16進制編輯器打開對應的類文件(ClassContent.class
),筆者這邊使用的是 010 Editor:
magic
前 4 個字節爲 .class
文件的魔數,用於做一個最簡單的文件類型校驗。比如你將一個 .jpg
類型的圖片文件後綴名改成 .class
時,這個 .class
文件是無效的,因爲第一步的魔數就不一樣。這個是一個固定的數據,對於其他的文件類型可能也存在,只不過值不一樣而已。在 .class
文件中值爲 CAFEBABY
(咖啡寶貝?),這個值非常有意思,因爲其意義正好對應 Java 的圖標:
來杯 82 年的 Java 壓壓驚?
minor_version
按字節順序繼續往下看,接下來的 2 個字節代表該 .class
文件要求裝載它的虛擬機的最低次版本號,這裏爲 0,證明只要虛擬機的主版本號不小於當前類文件的主版本號就可以加載這個類。
major_version
接下來兩個字節代表該 .class
文件要求裝載它的虛擬機的最低主版本號,這裏爲 0x0034
,即爲 10 進制的 52,JDK 的主版本號是從 45 開始的,接下去的每一個大版本號都爲之前的大版本號 + 1,高版本號的虛擬機可以向下兼容加載低版本號的類文件,但無法加載版本號比它更高的類文件。這個其實很好理解,因爲 Java 每一次升級都會帶來一些語法變化和一些新特性(Java 8 帶來了新的 API,lambda 表達式等),對應的會帶來支持這些新特性的虛擬機,而老版本的虛擬機並不支持這些新特性,所以爲了安全,虛擬機不允許加載版本號比自己的版本號高的類文件。
比如 JDK 1 能加載版本號爲 45.0 ~ 45.65535 的 .class
文件,但是無法加載版本號爲 46.0 ~ 46.65536 的 .class
文件。在這個例子中 .class
文件主版本號爲 52,次版本號爲 0,總版本號爲 52.0,證明其只能被 JDK 1.8 以上版本提供的虛擬機加載。
constant_pool
接下來的多個字節代表當前類的常量池信息,我們先看緊接着前面的兩個字節:0x002D
,這個值爲 10 進制的 45,意味着該類中的常量數爲 44,爲什麼是 44 呢?因爲 .class
中的常量的下標從 1 開始,那麼下標 0 代表的是什麼呢?代表的是某項數據不引用常量中的任何常量項目,這句話怎麼理解呢?我們可以從對象的值上理解,比如我們有一個 Person
類,現在我們定義了一個 Person
類的引用:Person person;
,我們暫時沒有 Person
類型的對象去給這個引用賦值,那麼我們會把它賦值爲 null:person = null;
。這就相當於不引用任何Personal類型的對象項目,那麼類文件中的常量池的下標 0 也是一樣的道理:如果類文件中某個項目引用到的常量的下標爲 0,證明這個項目不需要使用常量池中任何一個項目的值。
另外,常量池中的項目類型是多樣的,因爲常量本身就是多類型的(字符串常量、整形常量、浮點型常量、複雜對象類型常量…)下表列舉了所有常量池中的項目可能出現的類型:
類型 | 標誌 | 含義 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 編碼的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮點型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTANT_Class_info | 7 | 類或者接口的符號引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 字段或者方法的部分符號引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 標識方法類型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
非常繁瑣的是上面的每一種常量類型都有一個自定義的數據結構(表)類型,這些結構我們在碰到時再進行列舉。這些表結構有一個共性:表的第一個字節數據名爲 tag,代表的是當前常量的類型,(即爲上面常量類型表中的標誌)。我們來看上面 .class
文件的第一個常量類型(緊接着上面的常量數量字節後面的 1 個字節數據):0x0A
,即爲 10 進制的 10,對應的常量類型爲 CONSTANT_Methodref_info
,即爲一個描述方法的常量類型,那麼我們來看一下這種類型的常量的表結構:
類型 | 名稱 | 含義 |
---|---|---|
u1 | tag | 常量類型,值爲 10 |
u2 | class_index | 指向聲明這個方法的類描述符 CONSTANT_Class_info 的索引項 |
u2 | method_index | 指向描述該方法信息的描述符 CONSTANT_NameAnd_Type_info 的索引項 |
這個表的屬性也很好理解,既然這個表示用來描述一個類的方法的信息,那麼我們肯定需要知道定義這個方法的類的信息和這個方法本身的信息(方法名、參數個數和類型、返回值等),我們繼續看緊跟着 tag 的後兩個字節:0x0006
,即爲 10 進制的 6,也就是說這個方法所屬的類的信息儲存在常量池中第 6 個常量中,我們繼續看後面兩個字節:0x001E
,即爲 10 進制的 30,也就是說這個方法本身的信息在儲存在常量池中的第 30 個常量中。它們的具體值我們待會可以通過工具生成,這裏需要知道這個分析過程即可。我們到這裏已經分析完了常量池的第一個常量字段,下面我們來簡單看一下第二個常量,來鞏固一下這個分析過程。
緊接着第一個常量信息字節結束,第二個常量的 tag 值爲 ox09
,即爲 10 進制的 9,對應的常量類型爲 CONSTANT_Fieldref_info
類型,即爲一個描述字段信息的常量類型,我們來看一下這種類型的常量的表結構:
類型 | 名稱 | 含義 |
---|---|---|
u1 | tag | 常量類型,值爲 9 |
u2 | class_index | 指向聲明這個字段的類或接口的描述符 CONSTANT_Class_info 的索引項 |
u2 | field_index | 指向描述該字段信息的描述符 CONSTANT_NameAnd_Type_info 的索引項 |
和方法信息描述表類似,字段也有所屬的類和字段本身的信息(修飾符、類型、名稱)。我們來看接下來的 4 個字節,分析出這個字段的所屬類信息和其本身的信息,這兩個值分別爲 0x001F
和 0x0020
,對應 10 進制分別爲 31 和 32。也就是說這兩個信息儲存在常量池中第 31 個和第 32 個常量中。這兩個常量中我們都碰到了 CONSTANT_Class_info
和 CONSTANT_NameAnd_Type_info
這兩種常量類型,那麼我們來看一下他們的表結構,首先是 CONSTANT_Class_info
`:
類型 | 名稱 | 含義 |
---|---|---|
u1 | tag | 常量類型,值爲 7 |
u2 | index | 指向這個類的全限定名的常量項索引 |
接下來是 CONSTANT_NameAnd_Type_info
:
類型 | 名稱 | 含義 |
---|---|---|
u1 | tag | 常量類型,值爲 12 |
u2 | name_index | 指向該字段或者方法名稱的常量項的索引 |
u2 | descibe_index | 指向該字段或者方法描述符的常量項的索引 |
好了,有了上面的基礎之後,我們來藉助工具來看一下常量中的所有常量信息,我們在 ClassContent.class
所在的目錄下執行 javap -v ClassContent.class
命令行來獲取該類文件的詳細信息:
由於版面原因,這裏並沒有截取所有的字節碼,這裏我們關注常量池中的信息即可,我們可以看到剛剛我們分析的前兩個常量分別爲 CONSTANT_Methodref_info
和 CONSTANT_Fieldref_info
類型,和這裏生成的常量類型匹配,同時第一個方法描述常量對應的類信息爲第 6 個常量,第 6 個常量爲 CONSTANT_CLass_info
類型,其類的全限定名用一個 CONSTANT_Utf8_info
類型的常量來描述,這個常量類型的表結構如下:
類型 | 名稱 | 含義 |
---|---|---|
u1 | tag | 常量類型,值爲 1 |
u2 | length | UTF-8 編碼的字符串所佔用的字節數 |
u1 | bytes | 長度爲 length 並使用 UTF-8 編碼後的字符串數據,總體佔用 length 個字節 |
這裏的類的全限定名爲 java/lang/Object
,也就是這個方法是在 java.lang.Object
中定義的,我們再看方法的詳細信息,在第 30 個常量中,這個常量類型爲 CONSTANT_NameAnd_Type_info
類型,對應的方法名和描述分別爲 <init>
和 ()V
,還原之後爲 void <init>()
,這個方法本身在 Object
類中並沒有定義,這是因爲類在編譯時編譯器爲這個類自動生成的一個方法,類中的一些非靜態代碼塊和非靜態變量賦值操作都會移至該方法中執行。
接下來的是描述字段類型的常量,所屬的類信息在第 31 個常量中儲存,字段本身信息在第 32 個常量中儲存,繼續 “遞歸” 查找,發現這個字段所屬的類爲 java.lang.System
,字段名爲 out
,類型爲 java.io.PrintStream
。其實這個就是因爲我們代碼中調用了 System.out.println
方法導致的,在進行代碼編譯時,編譯器會將某個類中的常量值儲存在該常量調用類的常量池中。
總體看常量池中的常量信息,我們會發現能直接儲存“字面量”值的只有基本類型對應的常量數據類型(CONSTANT_Utf8_info
, CONSTANT_Integer_info
, CONSTANT_Float_info
, CONSTANT_Double_info
, CONSTANT_Long_info
)。那麼如果我們定義了一個 boolean
類型的常量會怎麼樣呢?它會被當作 int
類型的常量處理(byte
,char
,short
類型的常量也是如此 )。所有的複雜常量類型中的屬性真實值最終都是通過這幾個基本表中的值來儲存。最後給出所有常量類型的數據表結構(來自《深入理解 Java 虛擬機》):
access_flag
在常量池部分結束了之後,緊接着的兩個字節代表的是訪問標識,訪問標識即爲該類定義時的訪問權限,比如該類是否爲抽象類,是否爲接口,是否爲 final 類,是否爲枚舉等等。具體的標誌位和含義如下表:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 該類是否爲 public 類型 |
ACC_FINAL | 0x0010 | 該類是否爲 final 類 |
ACC_SUPER | 0x0020 | 是否允許使用 invokespecial 字節碼指令的新語義,invokespecial指令的語義在 JDK1.0.2 發生改變過,因此在 JDK 1.0.2 之後編譯出來的類中這個標誌一定爲真 |
ACC_INTERFACE | 0x0200 | 該類是否爲接口類型 |
ACC_ABSTRACT | 0x0400 | 是否爲 abstract 類型,對於接口和抽象類來說,這個標誌爲真,其他爲假 |
ACC_SYNTHETIC | 0x1000 | 標誌這個類並非由用戶代碼產生的(動態代理) |
ACC_ANNOTATION | 0x2000 | 標識這是一個註解 |
ACC_ENUM | 0x4000 | 標識這是一個枚舉 |
對於我們上面的 ClassContent
類來說,其是 public
修飾的,同時是使用 JDK 1.8 語法編譯的,因此它的 ACC_PUBLIC
和 ACC_SUPER
標誌爲真,即其 access_flag
的值應該爲 0x0021
,我們來看一下:
圖中紅色線條標註部分即爲 access_flag
標誌開始部分,我們可以看到這個結果和我們上面計算的相符。
this_class
緊接着 access_flag
後面的兩個字節爲 this_class
數據,表示的是當前類信息,其值作爲下標指向常量池的一個 CONSTANT_Class_info
類型的常量,這裏的值爲 0x0005
,即爲常量池中的第 5 個常量,我們回過頭去看看,第 5 個常量確實爲 CONSTANT_Class_info
類型,類的全限定名具體值爲第 36 個常量的值,即爲 ClassContent
。
super_class
接下來的兩個字節數據爲該類的父類的信息,值爲 0x0006
,同樣的我們去找一下常量池中第 6 個常量的信息,對應的類的全限定名儲存在第 37 個常量中,即爲 java/lang/Object
。也就是說 ClassContent
類的父類爲 Object
類,這也是毋庸置疑的。
interfaces_count
接下來的兩個字節數據爲該類實現的接口數量,如果該類本身是一個接口,則爲該接口繼承的接口數量。因爲 ClassContent
並沒有實現任何接口,因此這兩個字節的數據爲 0x0000
,即爲 0。
interfaces
接下來會有 interfaces_count
個 u2 類型的數據,指向的是描述該類實現 / 繼承的接口的信息常量在常量池中的下標(均爲 CONSTANT_Class_info
類型)。因爲在這裏 ClassContent
並沒有實現任何接口,因此這裏沒有任何數據。
fields_count
和 interfaces_count
屬性類似,接下來的兩個字節數據代表的是該類定義的字段數,在這裏爲 0x0002
,即爲兩個字段。
fileds
緊接着是 fileds_count
個 fields_info
表結構的數據,描述的是當前類定義的字段的信息,fields_info
表結構如下:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | access_flag | 1 | 字段的訪問標識 |
u2 | name_index | 1 | 字段名常量在常量池中的下標 |
u2 | descriptor_index | 1 | 字段類型在常量池中的下標 |
u2 | attributes_count | 1 | 額外屬性信息數量 |
attribute_info | attributes | attributes_count | 額外屬性信息 |
我們來繼續看字段信息數據,緊接着的兩個字節爲 access_flag
,這裏值爲 0x0019
,我們來看一下在字段中可能出現的訪問標誌:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
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_TRANSIENT | 0x0080 | 字段是否爲 transient |
ACC_SYNTHETIC | 0x1000 | 標誌這個字段由編譯器自動生成的 |
ACC_ENUM | 0x4000 | 標識字段是否爲一個枚舉 |
根據這個表,我們可以還原出第一個字段的修飾符:public static final
(10 + 8 + 1)。我們接着看下兩個字節(name_index)的數據:0x0007
,我們去找下標爲 7 的常量值:STR
,也就是說這個字段名爲 STR
,繼續看下兩個字節(descriptor_index)的數據:0x0008
。找到常量池中下標爲 8 的常量值:Ljava/lang/String;
意味這這個字段是 String
類型的。我們繼續看下兩個字節(attributes_count)的數據:0x0001
,意味着這個字段有 1 個額外屬性信息。那麼接下來的一部分字節數據就是用來描述這個額外的屬性表的信息,我們來看看 attribute_info
表的結構:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名在常量池中的常量下標 |
u4 | attributes_length | 1 | 屬性數據的長度 |
u1 | info | attributes_length | 屬性數據 |
我們接着看下兩個字節(attribute_name_index)的數據:0x0009
,在常量池中第 9 項常量的數據爲 ConstantValue
,意味着這個屬性表爲 ConstantValue
表,繼續看接下來四個字節(attributes_length)的數據:0x00000002
,意味着該屬性值的數據量爲 2 個字節,接下來兩個字節即爲該屬性的數據:0x0A00
,即爲 10 ,這個屬性表的數據分佈如下圖:
那麼這個 10 代表什麼意義呢?我們先來看看 ConstantValue
表的結構:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名在常量池中的常量下標 |
u4 | attributes_length | 1 | 屬性數據的長度 |
u1 | constantValue_index | attributes_length | 指向常量池中下標爲 constantValue_index 的常量 |
好了,我們知道了,上面的那個 10 代表的是常量池中的一個常量的下標,那麼我們來看看常量池中第 10 個常量是什麼:經過查找,第 10 個常量的字面量爲 s
,這不就是我們在代碼中定義的常量值嗎。至此,我們可以還原出這個字段的完整定義:public static final String STR = "s";
。我們已經解析了第一個常量,第二個常量也是同樣的道理,我們提取出描述第二個常量的數據:00 19 00 0B 00 0C 00 01 00 09 00 00 00 02 00 0D
。access_flag
值爲 0x0019
,字段名常量所在常量池下標爲 0x000B
即爲 11,字段類型常量所在常量池下標爲 00 0C
,得到的值爲 I
(即爲 int),常量表中常量值所在常量池的下表爲 0x000D
,得到的值爲1
。所以這個常量還原出來的代碼定義爲 public static final int A = 1;
。此外將常量值儲存在字段的額外信息中,待虛擬機加載該類時在準備階段時就可以將最終值賦值給該常量。而一般的靜態字段在準備階段時會被賦 0 值,在初始化階段中執行 <cinit>
方法時纔會賦最終值。
在上面屬性表中,屬性名儲存在常量池中,而屬性表本身通過 attribute_name_index 數據指向屬性表名字常量在常量池中的下標來記錄屬性表名稱。除了 ConstantValue
以外,Java 虛擬機還有很多其他類型的屬性表,最常見的便是 Code
表,它的結構我們在分析類文件中的方法屬性時會介紹,如果虛擬機在進行屬性表解析時發現屬性表名不是其可以識別(內置)的屬性表名時,則會略過這一屬性表數據的所有數據,繼續向後解析。
methods_count
字段信息解析完成之後,接下來的兩個字節的數據代表當前類的方法數,值爲 0x0003
:
也就是說這個類一共有三個方法,但是我們明明在代碼中只定義了 2 個方法啊,第三個方法怎麼來的?別忘了之前提到的 <init>
方法,編譯器在編譯每一個類時都會爲這個類提供一個 <init>
方法。
methods
緊接着便是方法的具體信息了,這裏先來看一下 method_info
表的格式:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | access_flag | 1 | 方法的訪問標識 |
u2 | name_index | 1 | 方法名常量在常量池中的下標 |
u2 | descriptor_index | 1 | 方法描述常量在常量池中的下標 |
u2 | attributes_count | 1 | 額外屬性信息數量 |
attribute_info | attributes | attributes_count | 額外屬性信息 |
方法可能存在的訪問標識如下:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 該方法是否爲 public 修飾 |
ACC_PRIVATE | 0x0002 | 該方法是否爲 private 修飾 |
ACC_PROTECTED | 0x0004 | 該方法是否爲 protected 修飾 |
ACC_STATIC | 0x0008 | 方法是否爲 static |
ACC_FINAL | 0x0010 | 方法是否爲 final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否爲 synchronized 修飾 |
ACC_BRIDGE | 0x0040 | 方法是否爲編譯器生成的橋接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定參數 |
ACC_NATIVE | 0x0100 | 方法是否爲 native |
ACC_ABSTRACT | 0x0400 | 方法是否爲抽象方法 |
ACC_STRICTFP | 0x0800 | 方法是否爲 strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否爲編譯器自動生成的 |
如果你理解了上一節字段相關的內容,那麼這兩個表格對你來說一點難度都沒有。我們繼續來看方法的內容,緊接着 methods_count
的兩個字節爲第一個方法的 access_flag
屬性值,爲 0x0001
,即爲 public
修飾的方法,接下來兩個字節爲 name_index
屬性值,爲 0x000E
,對應常量池中第 14 個常量,值爲 <init>
,即該方法的方法名爲 <init>
,接下來兩個字節爲 descriptor_index
的屬性值,爲 0x000F
,對應常量池中的第 15 個常量,即該方法的方法描述爲 ()V
,也就是說該方法沒有參數和返回值,那麼我們現在可以復原出這個方法了:public void <init>();
。我們接着來看 attributes_count
屬性的值,這裏爲 0x0001
,即該方法有一個額外的屬性表,屬性的表名爲 attribute_name_index
的值指向常量池對應下標的常量值,即爲下標爲 0x0010
的常量值,對應的常量池中的常量值爲 Code
。我們應該可以猜到這個 Code
屬性表就是儲存該方法代碼的字節碼的表結構了,我們來看看 Code
表的結構:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名在常量池中的常量下標 |
u4 | attributes_length | 1 | 屬性數據的長度 |
u2 | max_stack | 1 | 方法執行時操作數棧深度的最大值 |
u2 | max_locals | 1 | 方法執行時局部變量表所需要的存儲空間,這裏的單位 Slot,一個 Slot 可以儲存長度不超過32位的局部變量值 |
u4 | code_length | 1 | 代碼編譯成字節碼之後的代碼長度 |
u1 | code | code_length | 代碼內容,每一個字節數據對應一個字節碼 |
u2 | exception_table_length | 1 | 方法的異常表長度 |
exception_info | exception_table | exception_table_length | 方法拋出的異常信息 |
u2 | attribute_count | 1 | 額外屬性表數目 |
attribute_info | attributes | attribute_count | 額外屬性表信息 |
<init>
方法的 Code
屬性完整數據如下(底色爲深藍色的數據):
這個方法存在 5 個字節的字節碼數據,字節碼之所以被稱爲字節碼,就是因爲每一個字節碼都有一個字節的數據對應,通過一個字節的數據可以確定一個字節碼,Java 虛擬機已經定義了 200 多條字節碼指令(一個字節的數據範圍爲 0~255)。我們通過某個字節的數據就可以得到其所代表的字節碼。在這裏 <init>
方法中的 5 條字節碼指令分別爲:
aload_0: 將第一個引用類型本地變量推至操作數棧頂
invokespecial: 調用父類構造方法,實例初始化方法,似有方法(這裏爲調用父類構造方法)
nop: 什麼都不做
aconst_null: 將 null 推至操作數棧頂
return: 從當前方法返回 void
接下來的是方法的異常信息和額外信息,這部分篇幅過大了,這裏不細講了。小夥伴們可以參考 《深入理解 Java 虛擬機》原書。剩下的兩個方法小夥伴們可以自行分析。
attributes_count
緊接着方法字節數據結束後的兩個字節爲 attributes_count
數據,代表的是當前類的額外屬性表數量。在這個類文件中存在一個額外屬性表,這部分數據在類文件數據末尾:
attributes
緊接着上面的 attributes_count
數據後的數據爲當前類文件的唯一一個屬性表數據:
根據屬性表的一般結構,我們可以得到該屬性表的表名屬性(attribute_name_index)爲 0x001C
對應常量池中的常量爲 SourceFile
,也就是說這是一個 SourceFile
表,我們來看看 SourceFile
表的結構:
類型 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名在常量池中的常量下標 |
u4 | attributes_length | 1 | 屬性表中數據的長度 |
u2 | sourcefile_index | 1 | 指向常量池中下標爲 sourcefile_index 的常量 |
這裏我們可以得到該表中 sourcefile_index
的值爲 0x001D
,對應常量池中下標爲 29 的常量,即爲 ClassContent.java
,這也就是編譯出這個 .class
文件的 Java 文件的文件名。
與字段和方法中的屬性表相同,這裏的屬性表可以有多種類型,Java 虛擬機內置的屬性表的表名如下:
我們在上面已經講過了 Code
、ConstantValue
和 SourceFile
屬性表的結構,關於其他屬性表的結構就需要小夥伴們自己去參閱相關書籍和資料了。
再論類加載
回想一下我們在上篇文章中討論的 Java 類加載機制,需要經過五大步驟:加載、驗證、準備、解析、初始化。其實 驗證過程是最複雜的,因爲這個過程需要掃描整個在加載過程中得到得到的 .class
文件格式的二進制數據,也就是相當於將我們在上面模擬的解析 .class
文件的過程,並且判斷相關的數據是否合法,比如文件的魔數是否爲 CAFEBABY
,類的主次版本號是否不高於當前虛擬機的版本號,同時需要檢測字節碼的調用是否合法,比如一個局部變量是不是在未被賦初始值就被使用了,是否使用了不當的跳轉指令等等。總言之這部分工作需要確保該類在加載進入虛擬機之後在被使用的過程中不會出現致命性的問題以威脅虛擬機的正常運行狀態。
而在解析這一步中虛擬機需要將類中出現的符號引用替換爲直接引用,這個過程可能又會觸發其他類的加載,比如有兩個類 A 和類 B ,類 B 中有一個 A 類的引用,那麼在加載類 B 的時,在解析過程中發現引用了 類 A 的變量,那麼這時候可能會觸發虛擬機對類 A 的加載。
好了,在這篇文章中我們通過一個例子來看了一下類文件格式,相信你對 Java 類機制有了一個更深的理解。如果博客中有什麼不正確的地方,還請多多指點。如果覺得這篇文章對您有幫助,請不要吝嗇您的贊。
謝謝觀看。。。