很遺憾,這將是很枯燥的一章,但是如果想較為深入的理解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.多個同類數據項的描述:前置容量
由于類文件不采用分隔符的方式分隔數據,數據項的順序是被嚴格限定的,因此當需要描述多個同類數據項的時候,采用前置容量計數器的方式
類文件結構定義詳見下圖:
類文件結構詳解
通過上面的講解,我們對類文件結構有了一個宏觀的了解。接下來,我們通過一個簡單的類文件實例,來深入細節具體看一下類文件結構
首先,我們定義一個足夠簡單的Java類,詳見下圖:
之后將該類編譯后,通過十六進制方式查看TestClass.class文件。看著像亂碼?然而并不是
另外我們還可以通過javap命令,查看該文件的反匯編信息
1.魔數(magic)
魔數用來描述文件類型,是一個u4類型的數據(占據類文件的頭4個字節)
Java類文件的魔數值是0xCAFEBABE,看到這個是不是想起了Java的商標(咖啡)
使用魔數來表示文件類型,顯然比使用文件擴展名更加安全。虛擬機在讀取到0xCAFEBABE后則認為該文件是一個Class文件
2.版本號(version)
緊接著的4個字節代表的是類文件的版本號,其中前兩個字節代表次版本號(Minor Version),后兩個字節代表主版本號(Major Version)。通過版本號,虛擬機能夠檢查是否可兼容該類文件
查看十六進制類文件,看到次版本號是0x0000(十進制0),主版本號是0x0034(十進制52),我本地使用的編譯器版本是1.8.0。具體編譯器版本對應的十進制版本號請自行查閱,不在此贅述
3.常量池(constant_pool)
緊接著版本號的是常量池,常量池是類文件中第一個表類型的數據,其中數據項眾多,并且數量不定。前面我們說過,對于描述多個同類數據項的時候,采用前置容量計數器的方式。因此在常量池之前,是一個u2類型的數據,代表常量池容量計數(constant_pool_count)
查看TestClass類文件,常量池容量計數值是0x0016(十進制22),代表該類有21項常量,索引范圍是1-21(注意從1開始,而不是0)
常量池主要存放兩大類數據:字面量和符號引用
#字面量比較接近于Java語言層面常量的概念,比如字符串、聲明為final的常量值等
#符號引用主要包括:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。Java代碼編譯時,沒有“靜態連接”這一步驟,而是通過“動態連接”的方式。虛擬機在運行時,從常量池中獲取對應的符號引用,再翻譯到具體的內存地址
常量池中每一各數據項都對應一個表,一共有如下這些類型(14種),其中每一項開頭都包含一個u1類型的tag(下圖Value列),代表當前數據項代表的常量數據類型
下面我們繼續使用TestClass作為例子,看看常量池中數據是怎樣定義的:
首先我們看到的tag值是0x0a(十進制10),查閱上表,看到對應的是CONSTANT_Methodref_info,說明該常數項是方法的符號引用。我們看一下CONSTANT_Methodref_info的數據定義:
第一項是tag,上面說過了。第二項是class_index,代表擁有此方法的類的類信息在常量池中的索引。第三項是name_and_type_index,代表該方法的名稱和描述符信息在常量池中的索引
查看我們的類文件,class_index值是0x0004,說明常量池中第4項存放該類的類信息。name_and_type_index值是0x0012(十進制18),說明常量池中第18項存放名稱和描述符信息
另外,從上面提到的反匯編信息中,也可以更加明確地看出我們從十六進制類文件中分析出的內容
上面,我們通過查閱CONSTANT_Methodref_info的數據定義,并且對照十六進制類文件和反匯編信息,學習了怎樣讀懂類文件結構中的常量池信息。其實其他類型的常量和CONSTANT_Methodref_info一樣,都是類似的結構。下圖中選中的部分就是常量池相關的數據,有興趣可以按照上述的方法對照官方文檔逐一進行解析
4.訪問標志(access_flags)
在常量池之后,緊接著的兩個字節表示訪問標志。這個標志用于識別一些類或者接口層次的訪問信息。包括:該Class是否是public類型、是否被聲明為final、是否是一個接口、是否是注解、是否是枚舉等
完整定義如下:
其中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
5.類索引(this_class)、父類索引(super_class)、接口索引集合(interfaces)
類文件中通過這三項信息來確定這個類的繼承關系。其中類索引和父類索引都是u2類型的數據,接口索引集合是一組u2類型的數據(接口索引前會有一個u2類型數據表示接口索引的數量constant_pool_count)。這三類數據都指向常量池中的某項數據
類索引用來確定該類的全限定名;父類索引確定其父類的全限定名。Java是單繼承,所以父類索引只有一個;接口索引集合用來描述該類實現了哪些接口
繼續看我們的TestClass,類索引值為0x0003(十進制3),說明類信息在常量池的第三項,結合反匯編代碼,可以看到“class_structure/TestClass”
父類索引值為0x0004(十進制4),說明父類信息在常量池的第四項,結合反匯編代碼,可以看到TestClass繼承自“java/lang/Object”
接口索引的數量值為0x0000(十進制0),說明該類并沒有實現任何接口
6.字段表集合(fields)
字段表集合用于描述類中的變量(包括靜態變量、實例變量,但不包括局部變量)
每個字段通過一個field_info描述,field_info格式定義如下:
field_info中的access_flags作用及計算方式與類的access_flags類似,詳細定義如下:
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),說明沒有額外屬性
7.方法表集合(methods)
顧名思義,方法表集合用于描述類中的方法
如果理解了上一節的字段表集合,那么方法表集合就很好理解了,因為method_info在結構上與field_info極其類似。每個方法都通過一個method_info來描述
同樣,第一項是access_flags,詳細定義如下:
后續幾項: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”,說明此屬性是方法的字節碼描述
8.屬性表集合(attributes)
前面在講解類文件、字段表、方法表時曾多次出現屬性表這個概念,它的主要作用是用于描述某些場景下的專有信息
截止到java 8,屬性表集合中一共預定義了23種屬性,下面我們拿一些屬性作為例子進行講解,完整的介紹請參看官方文檔
對于每個屬性,屬性的名稱(attribute_name_index)引用常量池中的常量,屬性值(info)的結構完全自定義,只需要一個u4類型的長度屬性(attribute_length)來說明屬性值占用的字節數
#Code屬性
前面在介紹方法表的時候曾提到過Code屬性,其用于存儲方法體中編譯后的內容。但并非所有方法表都存在這個屬性,比如接口和抽象類中的抽象方法。Code屬性結構如下:
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種指令
下面我們再次通過我們的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,這條指令執行后方法結束
我們再次查看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異常
Exceptions屬性中的number_of_exceptions表示方法可能拋出的checked異常的數量。exception_index_table指向常量池中的常量,表示異常類型
#LineNumberTable屬性
LineNumberTable屬性用于描述源代碼行號與字節碼偏移量之間的對應關系。它雖然不是運行時必須的屬性,但是如果沒有相應的信息,那么程序拋出異常時,異常堆棧中將沒有行號,另外也無法按照源碼行來設置斷點
#LocalVariableTable屬性
LocalVariableTable屬性用于描述棧幀中局部變量表中變量與源碼中變量的關系。它也不是運行時必須的屬性,但是如果沒有相應信息,那么當其他人引用方法時,源碼中定義的參數名稱都將丟失,取而代之的是類似arg0、arg1這樣的的占位符
#Signature屬性
Signature屬性出現于類、字段表、方法表結構的屬性中,用于JDK1.5之后,記錄范型信息。之所以加入一個屬性記錄范型信息,是因為Java中范型采用的是擦除法實現的偽范型。在字節碼中,范型信息會被擦除,優點是實現簡單(主要修改編譯器,虛擬機很少改動),但缺點就是運行期間無法獲得范型信息。Signature屬性就是為了彌補這個缺陷增設的
行文至此,我想類文件的結構原理已經基本描述清楚了,其余沒有講到的結構也都是類似,如果有興趣或者日后有需要用到,可以再做詳細的了解
筆記3結束