《深入理解Java虛擬機器》讀書筆記3--類檔案結構

weixin_33866037發表於2017-12-03

很遺憾,這將是很枯燥的一章,但是如果想較為深入的理解JVM,這一章又很有必要硬著頭皮搞清楚。如果之前沒有接觸過類似的內容,那麼有很大的可能第一次基本讀不懂,如果出現這樣的情況也沒有關係,請繼續保持學習,並且隔段時間再次重新閱讀。像我這樣不夠靈光的腦袋,學習了3遍也就能夠掌握基本原理。其實,只要掌握了對應的規則,類檔案的內容又是很容易解讀的,請保持你的耐心與好奇

Java號稱跨平臺,那麼究竟是什麼能夠使Java跨平臺?簡單來說就是兩點:第一是編譯器能夠將原始碼編譯成某種平臺無關的格式;第二是能夠將該種格式翻譯成具體平臺指令集的虛擬機器

而這種平臺無關的格式就是位元組碼。虛擬機器不與包括Java語言在內的任何語言繫結,它只與位元組碼關聯。因此也就誕生了後續眾多基於JVM的新型語言

完整的類檔案結構說明請參考“官網文件:The class File Format

類檔案結構

1.資料組織方式:緊湊的二進位制

類檔案是一組以位元組為基礎的二進位制資料,各資料項嚴格按照定義排列,中間沒有分隔符及填充。如果遇到8位以上的資料項時,則按照大端法(Big-Endian,關於大端法可參考“理解位元組序 - 阮一峰的網路日誌”)拆分成若干個位元組儲存

2.資料型別:無符號數和表

#無符號數,用來描述數字、索引或者UTF-8編碼的字串值。u1、u2、u4、u8分別代表1個位元組、2個位元組、4個位元組、8個位元組的無符號數

#表,由多個無符號數或其他表組合成複合資料結構。Class檔案本質上就是一張表

3.多個同類資料項的描述:前置容量

由於類檔案不採用分隔符的方式分隔資料,資料項的順序是被嚴格限定的,因此當需要描述多個同類資料項的時候,採用前置容量計數器的方式

9011129-5414a2658e3affe7.png

類檔案結構定義詳見下圖:

9011129-97dfb2a5676345f7.png

類檔案結構詳解

通過上面的講解,我們對類檔案結構有了一個巨集觀的瞭解。接下來,我們通過一個簡單的類檔案例項,來深入細節具體看一下類檔案結構

首先,我們定義一個足夠簡單的Java類,詳見下圖:

9011129-7362d4ef5afd5caa.png

之後將該類編譯後,通過十六進位制方式檢視TestClass.class檔案。看著像亂碼?然而並不是

9011129-5a07cd8a02d2d398.png

另外我們還可以通過javap命令,檢視該檔案的反彙編資訊

9011129-fa5705c2a0f924d1.png
9011129-374585cfc2eb44e5.png

1.魔數(magic)

魔數用來描述檔案型別,是一個u4型別的資料(佔據類檔案的頭4個位元組)

9011129-b303d3dcffc01cd3.png

Java類檔案的魔數值是0xCAFEBABE,看到這個是不是想起了Java的商標(咖啡)

9011129-b580177e6aaa12d6.png

使用魔數來表示檔案型別,顯然比使用副檔名更加安全。虛擬機器在讀取到0xCAFEBABE後則認為該檔案是一個Class檔案

9011129-879266eec3a1e086.png

2.版本號(version)

緊接著的4個位元組代表的是類檔案的版本號,其中前兩個位元組代表次版本號(Minor Version),後兩個位元組代表主版本號(Major Version)。通過版本號,虛擬機器能夠檢查是否可相容該類檔案

9011129-ac1956a905add4b3.png

檢視十六進位制類檔案,看到次版本號是0x0000(十進位制0),主版本號是0x0034(十進位制52),我本地使用的編譯器版本是1.8.0。具體編譯器版本對應的十進位制版本號請自行查閱,不在此贅述

9011129-6bca3a4973abac24.png

3.常量池(constant_pool)

緊接著版本號的是常量池,常量池是類檔案中第一個表型別的資料,其中資料項眾多,並且數量不定。前面我們說過,對於描述多個同類資料項的時候,採用前置容量計數器的方式。因此在常量池之前,是一個u2型別的資料,代表常量池容量計數(constant_pool_count)

9011129-2e7dfdca9376e6c4.png

檢視TestClass類檔案,常量池容量計數值是0x0016(十進位制22),代表該類有21項常量,索引範圍是1-21(注意從1開始,而不是0)

9011129-ec2b8f6b48b5da58.png

常量池主要存放兩大類資料:字面量和符號引用

#字面量比較接近於Java語言層面常量的概念,比如字串、宣告為final的常量值等

#符號引用主要包括:類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符。Java程式碼編譯時,沒有“靜態連線”這一步驟,而是通過“動態連線”的方式。虛擬機器在執行時,從常量池中獲取對應的符號引用,再翻譯到具體的記憶體地址

常量池中每一各資料項都對應一個表,一共有如下這些型別(14種),其中每一項開頭都包含一個u1型別的tag(下圖Value列),代表當前資料項代表的常量資料型別

9011129-5f816ffec2823b60.png

下面我們繼續使用TestClass作為例子,看看常量池中資料是怎樣定義的:

首先我們看到的tag值是0x0a(十進位制10),查閱上表,看到對應的是CONSTANT_Methodref_info,說明該常數項是方法的符號引用。我們看一下CONSTANT_Methodref_info的資料定義:

9011129-24f47d22952f33d5.png

第一項是tag,上面說過了。第二項是class_index,代表擁有此方法的類的類資訊在常量池中的索引。第三項是name_and_type_index,代表該方法的名稱和描述符資訊在常量池中的索引

檢視我們的類檔案,class_index值是0x0004,說明常量池中第4項存放該類的類資訊。name_and_type_index值是0x0012(十進位制18),說明常量池中第18項存放名稱和描述符資訊

9011129-2b19c034f9a62cb0.png

另外,從上面提到的反彙編資訊中,也可以更加明確地看出我們從十六進位制類檔案中分析出的內容

9011129-8733fb1616bfa30a.png

上面,我們通過查閱CONSTANT_Methodref_info的資料定義,並且對照十六進位制類檔案和反彙編資訊,學習了怎樣讀懂類檔案結構中的常量池資訊。其實其他型別的常量和CONSTANT_Methodref_info一樣,都是類似的結構。下圖中選中的部分就是常量池相關的資料,有興趣可以按照上述的方法對照官方文件逐一進行解析

9011129-03e571ba0fdc6e6e.png

4.訪問標誌(access_flags)

在常量池之後,緊接著的兩個位元組表示訪問標誌。這個標誌用於識別一些類或者介面層次的訪問資訊。包括:該Class是否是public型別、是否被宣告為final、是否是一個介面、是否是註解、是否是列舉等

9011129-6f29e4a3b60f2ad3.png

完整定義如下:

9011129-857097894d954268.png

其中ACC_SUPER代表是否允許invokespecial指令的新語義。invokespecial在JDK 1.0.2版本發生過改變,因此為了區分這條指令使用哪種語意,JDK 1.0.2之後該標誌位都為0x0020。對於1.8及以上版本,無論該標誌位是否被設定,JVM都會統一認為該標誌位為真

我們例項中的TestClass,僅被定義為public,並且我當前使用的是1.8版本的JDK,因此ACC_PUBLIC及ACC_SUPER會被設定,其他標誌位都為0。最終訪問標誌位的值會被設定為0x0001 | 0x0020 = 0x0021

9011129-2b1520d9a4733c9f.png

5.類索引(this_class)、父類索引(super_class)、介面索引集合(interfaces)

類檔案中通過這三項資訊來確定這個類的繼承關係。其中類索引和父類索引都是u2型別的資料,介面索引集合是一組u2型別的資料(介面索引前會有一個u2型別資料表示介面索引的數量constant_pool_count)。這三類資料都指向常量池中的某項資料

9011129-da738d8b8f975be9.png

類索引用來確定該類的全限定名;父類索引確定其父類的全限定名。Java是單繼承,所以父類索引只有一個;介面索引集合用來描述該類實現了哪些介面

繼續看我們的TestClass,類索引值為0x0003(十進位制3),說明類資訊在常量池的第三項,結合反彙編程式碼,可以看到“class_structure/TestClass”

父類索引值為0x0004(十進位制4),說明父類資訊在常量池的第四項,結合反彙編程式碼,可以看到TestClass繼承自“java/lang/Object”

介面索引的數量值為0x0000(十進位制0),說明該類並沒有實現任何介面

9011129-e35309ea80c3a824.png
9011129-06c2b2e23739f657.png

6.欄位表集合(fields)

欄位表集合用於描述類中的變數(包括靜態變數、例項變數,但不包括區域性變數)

9011129-163dbe4f93943d4d.png

每個欄位通過一個field_info描述,field_info格式定義如下:

9011129-745b9c33e8d88af5.png

field_info中的access_flags作用及計算方式與類的access_flags類似,詳細定義如下:

9011129-86989f64cf48256f.png

access_flags之後是name_index和descriptor_index,分別代表欄位的簡單名稱索引及描述符索引,他們都是對常量池中常量的引用。之後是attributes方面的內容,後面再做介紹

下面來看一下TestClass,fields_count值為0x0001(十進位制1),代表只有一個欄位(private int m;);access_flags值為0x0002(十進位制2),對照上面的access_flags定義表,發現只有ACC_PRIVATE為真,所以值為0x0002;name_index值為0x0005(十進位制5),說明欄位的簡單名稱引用常量池中第5項;descriptor_index值為0x0006(十進位制6),說明欄位的描述符引用常量池中第6項(反彙編程式碼常量第6項的“I”代表基本型別int);attributes_count值為0x0000(十進位制0),說明沒有額外屬性

9011129-9e6d6b75b6d709ce.png
9011129-4ec4765c830137c0.png

7.方法表集合(methods)

顧名思義,方法表集合用於描述類中的方法

9011129-ac150b1e321c2d86.png

如果理解了上一節的欄位表集合,那麼方法表集合就很好理解了,因為method_info在結構上與field_info極其類似。每個方法都通過一個method_info來描述

9011129-f8ed4abbcecd8dbd.png

同樣,第一項是access_flags,詳細定義如下:

9011129-050cd186545334f8.png

後續幾項:name_index、descriptor_index、attributes含義都與欄位表中類似,只不過在方法表中這些欄位用於描述方法而已

也許你會有所疑問:方法裡面的程式碼在哪裡?方法裡面的程式碼,存放在屬性表集合中一個名為“Code”的屬性裡。關於屬性表的內容,後面我們再做講解

繼續回到TestClass,methods_count值為0x0002(十進位制2),代表有兩個方法(其中一個是編譯器自動新增的例項構造器<init>,另一個是我們自己定義的public int inc()方法);第一個方法的access_flags值為0x0001(十進位制1),對照上面的access_flags定義表,發現只有ACC_PUBLIC為真,所以值為0x0001;name_index值為0x0007(十進位制7),說明方法名稱引用常量池中第5項;descriptor_index值為0x0008(十進位制8),說明方法的描述符引用常量池中第8項(反彙編程式碼常量池第8項的“()V”代表void方法);attributes_count值為0x0001(十進位制1),說明該方法的屬性表集合有一項屬性,索引為0x0009(十進位制9),對應常量池中第9項常量為“Code”,說明此屬性是方法的位元組碼描述

9011129-97a6ae98802b5964.png

8.屬性表集合(attributes)

前面在講解類檔案、欄位表、方法表時曾多次出現屬性表這個概念,它的主要作用是用於描述某些場景下的專有資訊

截止到java 8,屬性表集合中一共預定義了23種屬性,下面我們拿一些屬性作為例子進行講解,完整的介紹請參看官方文件

9011129-10e08ff79d490152.png

對於每個屬性,屬性的名稱(attribute_name_index)引用常量池中的常量,屬性值(info)的結構完全自定義,只需要一個u4型別的長度屬性(attribute_length)來說明屬性值佔用的位元組數

9011129-bce0c673267a245d.png

#Code屬性

前面在介紹方法表的時候曾提到過Code屬性,其用於儲存方法體中編譯後的內容。但並非所有方法表都存在這個屬性,比如介面和抽象類中的抽象方法。Code屬性結構如下:

9011129-592942575e88bc42.png

1)attribute_name_index和attribute_length上面已經講過

2)max_stack代表運算元棧的最大深度,虛擬機器需要根據這個值來分配棧幀中運算元棧的深度

3)max_locals代表了區域性變數表所需的儲存空間。max_locals的單位是slot,slot是虛擬機器為區域性變數分配記憶體的最小單位。對於32位的資料型別(byte、char、short、int、float、boolean、returnAddress),每個區域性變數佔用一個slot,而對於64位的資料型別(long、double)則需要佔用兩個slot。另外,max_locals的值並不是方法中定義了多少個區域性變數,就把相應占用的slot數量簡單相加。原因在於,當程式碼執行超出了某個變數的作用域之後,它所佔用的slot就可以被其他的區域性變數所佔用,因此slot實際是可以複用的。編譯器會根據作用域給本地變數分配slot,然後計算出max_locals的值

4)code_length和code用來儲存編譯器編譯後的位元組碼指令(類似於彙編指令)。code_length代表位元組碼的長度,code則是一系列位元組碼流。每個位元組碼指令佔用一個位元組,當虛擬機器讀取到code中的一個位元組,會根據位元組碼指令表找到對應的指令,並且可以知道這條指令後面是否會跟隨引數,以及引數數量和具體含義。1個位元組取值範圍是0x00(十進位制0)~0xFF(十進位制255),也就是說一共可以表示256種指令

9011129-31935e1c167f4acb.png

下面我們再次通過我們的TestClass來看一下code_length和code是如何定義的。首先code_length值為0x00000005(十進位制5),說明後續五個位元組是code

code中第一個位元組值是0x2A,查表得知對應指令為aload_0,該指令含義是將第0個slot中的引用型別的本地變數推入運算元棧頂

code中第二個位元組值是0xB7,查表得知對應指令為invokespecial,該指令含義是將運算元棧頂的引用資料所指向的物件作為方法接收者,呼叫該物件的例項構造方法、private方法或者他父類的方法

code中第三和第四個位元組值是0x0001(十進位制1),這個u2型別的資料是前面invokespecial指令的引數,它指向常量池中第一個常量,代表具體呼叫哪個方法。檢視反彙編程式碼,可以看到對應的是“java/lang/Object."<init>":()V”,代表呼叫父類Object的例項構造方法

code中第五個位元組值是0xB1,查表得知對應指令為return,含義是從當前方法返回void,這條指令執行後方法結束

9011129-f97196da57de8098.png
9011129-48ac8f48e79332de.png

我們再次檢視TestClass的反彙編程式碼,看到其中兩個方法(例項構造方法和inc方法)args_size的值都是1,但是這兩個方法實際上都是無參的。另外無論是引數列表還是方法體內,都沒有定義任何區域性變數,但是locals也都是1。這是因為,在例項方法內,我們可以通過this關鍵字訪問此方法所屬的物件,而this正是通過編譯器在方法呼叫時通過方法引數自動傳入的。如果inc方法是static的,那麼args_size就是0了

5)exception_table_length和exception_table用來描述異常處理資訊。exception_table中一共包含4項資訊,含義是:如果在start_pc到end_pc(不含)位置出現了型別為catch_type(包含其子類)的異常,則轉向handler_pc處進行處理

#Exceptions屬性

這裡的Exceptions屬性與上面講到的Code屬性裡的exception_table不是一回事兒,這裡的Exceptions屬性與Code屬性平級,代表該方法可能丟擲的checked異常

9011129-47af3b060543b26d.png

Exceptions屬性中的number_of_exceptions表示方法可能丟擲的checked異常的數量。exception_index_table指向常量池中的常量,表示異常型別

#LineNumberTable屬性

LineNumberTable屬性用於描述原始碼行號與位元組碼偏移量之間的對應關係。它雖然不是執行時必須的屬性,但是如果沒有相應的資訊,那麼程式丟擲異常時,異常堆疊中將沒有行號,另外也無法按照原始碼行來設定斷點

9011129-300e8255cce99081.png

#LocalVariableTable屬性

LocalVariableTable屬性用於描述棧幀中區域性變數表中變數與原始碼中變數的關係。它也不是執行時必須的屬性,但是如果沒有相應資訊,那麼當其他人引用方法時,原始碼中定義的引數名稱都將丟失,取而代之的是類似arg0、arg1這樣的的佔位符

9011129-ea3c4fc8596a25f3.png

#Signature屬性

Signature屬性出現於類、欄位表、方法表結構的屬性中,用於JDK1.5之後,記錄範型資訊。之所以加入一個屬性記錄範型資訊,是因為Java中範型採用的是擦除法實現的偽範型。在位元組碼中,範型資訊會被擦除,優點是實現簡單(主要修改編譯器,虛擬機器很少改動),但缺點就是執行期間無法獲得範型資訊。Signature屬性就是為了彌補這個缺陷增設的

9011129-cdea85de08250089.png

行文至此,我想類檔案的結構原理已經基本描述清楚了,其餘沒有講到的結構也都是類似,如果有興趣或者日後有需要用到,可以再做詳細的瞭解

筆記3結束

相關文章