後端--Java中class檔案結構
最近剛看完《深入理解Java虛擬機器》周志明著 第六章 類檔案結構,在這裡寫一篇關於JVM如何解析Class檔案結構的部落格。
Class類檔案結構
- Class檔案是一組以8位元組為基礎單位的二進位制流,
- 各個資料專案嚴格按照順序緊湊排列在class檔案中,
- 中間沒有任何分隔符,這使得class檔案中儲存的內容幾乎是全部程式執行的程式。
Java虛擬機器規範規定,Class檔案格式採用類似C語言結構體的偽結構來儲存資料,這種結構只有兩種資料型別:無符號數和表。
無符號數
屬於基本資料型別,主要可以用來描述數字、索引符號、數量值或者按照UTF-8編碼構成的字串值,大小使用u1、u2、u4、u8分別表示1位元組、2位元組、4位元組和8位元組。
表
是由多個無符號數或者其他表作為資料項構成的複合資料型別,所有的表都習慣以“_info”結尾。表主要用於描述有層次關係的複合結構的資料,比如方法、欄位。需要注意的是class檔案是沒有分隔符的,所以每個的二進位制資料型別都是嚴格定義的。具體的順序定義如下:
在class檔案中,主要分為魔數、Class檔案的版本號、常量池、訪問標誌、類索引(還包括父類索引和介面索引集合)、欄位表集合、方法表集合、屬性表集合。
魔數
- 每個Class檔案的頭4個位元組稱為魔數(Magic Number)
- 唯一作用是用於確定這個檔案是否為一個能被虛擬機器接受的Class檔案。
- Class檔案魔數的值為0xCAFEBABE。如果一個檔案不是以0xCAFEBABE開頭,那它就肯定不是Java class檔案。
很多檔案儲存標準中都使用魔數來進行身份識別,譬如圖片格式,如gif或jpeg等在檔案頭中都存有魔數。使用魔術而不是使用副檔名是基於安全性考慮的——副檔名可以隨意被改變!!!
Class檔案的版本號
緊接著魔數的4個位元組是Class檔案版本號,版本號又分為:
- 次版本號(minor_version): 前2位元組用於表示次版本號
- 主版本號(major_version): 後2位元組用於表示主版本號。
這個的版本號是隨著jdk版本的不同而表示不同的版本範圍的。Java的版本號是從45開始的。如果Class檔案的版本號超過虛擬機器版本,將被拒絕執行。
0X0034(對應十進位制的50):JDK1.8
0X0033(對應十進位制的50):JDK1.7
0X0032(對應十進位制的50):JDK1.6
0X0031(對應十進位制的49):JDK1.5
0X0030(對應十進位制的48):JDK1.4
0X002F(對應十進位制的47):JDK1.3
0X002E(對應十進位制的46):JDK1.2
ps:0X表示16進位制
常量池
緊接著魔數與版本號之後的是常量池入口.常量池簡單理解為class檔案的資源從庫
- 是Class檔案結構中與其它專案關聯最多的資料型別
- 是佔用Class檔案空間最大的資料專案之一
- 是在檔案中第一個出現的表型別資料專案
由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2型別的資料,代表常量池容量計數值(constant_pool_count)。
從1開始計數。Class檔案結構中只有常量池的容量計數是從1開始的,第0項騰出來滿足後面某些指向常量池的索引值的資料在特定情況下需要表達"不引用任何一個常量池專案"的意思,這種情況就可以把索引值置為0來表示(留給JVM自己用的)。但儘管constant_pool列表中沒有索引值為0的入口,缺失的這一入口也被constant_pool_count計數在內。例如,當constant_pool中有14項,constant_poo_count的值為15。
常量池之中主要存放兩大類常量:
- 字面量: 比較接近於Java語言層面的常量概念,如文字字串、被宣告為final的常量值等
- 符號引用: 屬於編譯原理方面的概念,包括了下面三類常量:
-
- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
Java程式碼在進行Java編譯的時候,並不像C和C++那樣有"連線"這一步驟,而是在虛擬機器載入Class檔案的時候進行動態連線。也就是說,在Class檔案中不會儲存各個方法和欄位的最終記憶體佈局資訊,因此這些欄位和方法的符號引用不經過轉換的話是無法被虛擬機器使用的。當虛擬機器執行時,需要從常量池獲得對應的符號引用,再在類建立時或執行時解析並翻譯到具體的記憶體地址之中。
constant_pool_count:佔2位元組,本例為0x0016,轉化為十進位制為22,即說明常量池中有21個常量(只有常量池的計數是從1開始的,其它集合型別均從0開始),索引值為1~21。第0項常量具有特殊意義,如果某些指向常量池索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的含義,這種情況可以將索引值置為0來表示
constant_pool:表型別資料集合,即常量池中每一項常量都是一個表,共有14種(JDK1.7前只有11種)結構各不相同的表結構資料。這14種表都有一個共同的特點,即均由一個u1型別的標誌位開始,可以通過這個標誌位來判斷這個常量屬於哪種常量型別,常量型別及其資料結構如下表所示:
譬如utf-8型別的表結構資料
譬如fieldref型別的表結構資料
譬如class型別的表結構資料
譬如nameandtype型別的表結構資料
ps:什麼是描述符?
成員變數(包括靜態成員變數和例項變數) 和方法都有各自的描述符。
對於欄位而言,描述符用於描述欄位的資料型別;
對於方法而言,描述符用於描述欄位的資料型別、引數列表、返回值。
在描述符中,基本資料型別用大寫字母表示,物件型別用“L物件型別的全限定名”表示,陣列用“[陣列型別的全限定名”表示。
描述方法時,將引數根據上述規則放在()中,()右側按照上述方法放置返回值。而且引數之間無需任何符號。
訪問標誌(2位元組)
常量池之後的資料結構是訪問標誌(access_flags),這個標誌主要用於識別一些類或介面層次的訪問資訊,主要包括:
- 是否final
- 是否public,否則是private
- 是否是介面
- 是否可用invokespecial位元組碼指令
- 是否是abstact
- 是否是註解
- 是否是列舉
access_flags一共有16個標誌位可以使用,當前只定義了其中8個(JDK1.5增加後面3種),沒有使用到標誌位一律為0。
類索引、父類索引和介面索引集合
這三項資料主要用於確定這個類的繼承關係。
其中類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引(interface)集合是一組u2型別的資料。(多實現單繼承)
類索引(this_class),用於確定這個類的全限定名,佔2位元組
父類索引(super_class),用於確定這個類父類的全限定名(Java語言不允許多重繼承,故父類索引只有一個。除了java.lang.Object類之外所有類都有父類,故除了java.lang.Object類之外,所有類該欄位值都不為0),佔2位元組
介面索引計數器(interfaces_count),佔2位元組。如果該類沒有實現任何介面,則該計數器值為0,並且後面的介面的索引集合將不佔用任何位元組,
介面索引集合(interfaces),一組u2型別資料的集合。用來描述這個類實現了哪些介面,這些被實現的介面將按implements語句(如果該類本身為介面,則為extends語句)後的介面順序從左至右排列在介面的索引集合中
this_class、super_class與interfaces按順序排列在訪問標誌之後,它們中儲存的索引值均指向常量池中一個CONSTANT_Class_info型別的常量,通過這個常量中儲存的索引值可以找到定義在CONSTANT_Utf8_info型別的常量中的全限定名字串
欄位表集合
fields_count:欄位表計數器,即欄位表集合中的欄位表資料個數,佔2位元組。本測試類其值為0x0001,即只有一個欄位表資料,也就是測試類中只包含一個變數(不算方法內部變數)
fields:欄位表集合,一組欄位表型別資料的集合。欄位表用於描述介面或類中宣告的變數,包括類級別(static)和例項級別變數,不包括在方法內部宣告的變數
在Java中一般通過如下幾項描述一個欄位:欄位作用域(public、protected、private修飾符)、是類級別變數還是例項級別變數(static修飾符)、可變性(final修飾符)、併發可見性(volatile修飾符)、可序列化與否(transient修飾符)、欄位資料型別(基本型別、物件、陣列)以及欄位名稱。在欄位表中,變數修飾符使用標誌位表示,欄位資料型別和欄位名稱則引用常量池中常量表示。
型別 |
名稱 |
數量 |
說明 |
u2 |
access_flags |
1 |
修飾符標記位 |
u2 |
name_index |
1 |
代表欄位的簡單名稱,佔2位元組,是一個對常量池的引用 |
u2 |
descriptor_index |
1 |
代表欄位的型別,佔2個位元組,是一個對常量池的引用 |
u2 |
attributes_count |
1 |
屬性計數器 |
attribute_info |
attributes |
attributes_count |
屬性表集合 |
欄位表包含的固定資料項到descriptor_index結束,之後跟隨一個屬性表集合用於儲存一些附加資訊。
欄位表集合中不會列出從父類或父介面中繼承的欄位,但是可能列出原本Java程式碼之中不存在的欄位,如:內部類為了保持對外部類的訪問性,自動新增指向外部類例項的欄位。Java語言中欄位是不能過載的,2個欄位無論資料型別、修飾符是否相同,都不能使用相同的名稱;但是對於位元組碼,只要欄位描述符不同,欄位重名就是合法的。
方法表集合
methods_count:方法表計數器,即方法表集合中的方法表資料個數。佔2位元組,其值為0x0002,即測試類中有2個方法
methods:方法表集合,一組方法表型別資料的集合。方法表結構和欄位表結構一樣。
2個位元組為屬性計數器,其值為0x0001,說明這個方法的屬性表集合中有一個屬性(詳細說明見後面“屬性表集合”)
屬性名稱為接下來2個位元組0x0009,指向常量池中第9個常量,Code。
接下來4個位元組為0x0000002F,表示Code屬性值的位元組長度為47。
接下來2個位元組為0x0001,表示該方法的運算元棧的深度最大值為1。
接下來2個位元組依然為0x0001,表示該方法的區域性變數佔用空間為1。
接下來4個位元組為0x00000005,則緊接著的5個位元組0x2AB70001B1為該方法編譯後生成的位元組碼指令。
接下來2個位元組為0x0000,說明Code屬性異常表集合為空。
接下來2個位元組為0x0002,說明Code屬性帶有2個屬性,
接下來2個位元組0x000A即為Code屬性第一個屬性的屬性名稱,指向常量池中第10個常量:LineNumberTable。
接下來4個位元組為0x00000006,表示LineNumberTable屬性值所佔位元組長度為6。
接下來2個位元組為0x0001,line_number_table中只有一個line_number_info表,start_pc為0x0000,line_number為0x0003,LineNumberTable屬性結束。
接下來2位0x000B為Code屬性第二個屬性的屬性名,指向常量池中第11個常量:LocalVariableTable。該屬性值所佔的位元組長度為0x0000000C=12。
接下來2位為0x0001,說明local_variable_table中只有一個local_variable_info表,按照local_variable_info表結構,start_pc為0x0000,length為0x0005,name_index為0x000C,指向常量池中第12個常量:this,descriptor_index為0x000D,指向常量池中第13個常量:LTestClass;,index為0x0000。
ps:
如果子類沒有重寫父類的方法,方法表集合中就不會出現父類方法的資訊;有可能會出現由編譯器自動新增的方法(如:最典型的<init>,例項類構造器)在Java語言中,過載一個方法除了要求和原方法擁有相同的簡單名稱外,還要求必須擁有一個與原方法不同的特徵簽名(,由於特徵簽名不包含返回值,故Java語言中不能僅僅依靠返回值的不同對一個已有的方法過載;但是在Class檔案格式中,特徵簽名即為方法描述符,只要是描述符不完全相同的2個方法也可以合法共存,即2個除了返回值不同之外完全相同的方法在Class檔案中也可以合法共存。
注意:Java程式碼的方法特徵簽名只包括方法名稱、引數順序、引數型別。 而位元組碼的特徵簽名還包括方法返回值和受異常表。
屬性表集合
起始2個位元組為0x0001,說明有一個類屬性。
接下來2個位元組為屬性的名稱,0x0010,指向常量池中第16個常量:SourceFile。
接下來4個位元組為0x00000002,說明屬性體長度為2位元組。
最後2個位元組為0x0011,指向常量池中第27個常量:TestClass.java,即這個Class檔案的原始碼檔名為TestClass.java
與Class檔案中其它資料項對長度、順序、格式的嚴格要求不同,屬性表集合不要求其中包含的屬性表具有嚴格的順序,並且只要屬性的名稱不與已有的屬性名稱重複,任何人實現的編譯器可以向屬性表中寫入自己定義的屬性資訊。虛擬機器在執行時會忽略不能識別的屬性,為了能正確解析Class檔案,虛擬機器規範中預定義了虛擬機器實現必須能夠識別的9項屬性(預定義屬性已經增加到21項):
屬性名稱 |
使用位置 |
含義 |
Code |
方法表 |
Java程式碼編譯成的位元組碼指令 |
ConstantValue |
欄位表 |
final關鍵字定義的常量值 |
Deprecated |
類檔案、欄位表、方法表 |
被宣告為deprecated的方法和欄位 |
Exceptions |
方法表 |
方法丟擲的異常 |
InnerClasses |
類檔案 |
內部類列表 |
LineNumberTale |
Code屬性 |
Java原始碼的行號與位元組碼指令的對應關係 |
LocalVariableTable |
Code屬性 |
方法的區域性變數描述(區域性變數作用域) |
SourceFile |
類檔案 |
原始檔名稱 |
Synthetic |
類檔案、方法表、欄位表 |
標識方法或欄位是由編譯器自動生成的 |
ps:在除錯是可以通過SourceFile來關聯相關的類。
大總結的PS:
1,全限定名:將類全名中的“.”替換為“/”,為了保證多個連續的全限定名之間不產生混淆,在最後加上“;”表示全限定名結束。例如:"com.test.Test"類的全限定名為"com/test/Test;"
2,簡單名稱:沒有型別和引數修飾的方法或欄位名稱。例如:"public void add(int a,int b){...}"該方法的簡單名稱為"add","int a = 123;"該欄位的簡單名稱為"a"
3,描述符:描述欄位的資料型別、方法的引數列表(包括數量、型別和順序)和返回值。根據描述符規則,基本資料型別和代表無返回值的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","String[][]"將被記錄為"[[Ljava/lang/String;"
用描述符描述方法時,按照先引數列表,後返回值的順序描述,引數列表按照引數的嚴格順序放在一組"()"之內,如:方法"String getAll(int id,String name)"的描述符為"(I,Ljava/lang/String;)Ljava/lang/String;"
4,Slot,虛擬機器為區域性變數分配記憶體所使用的最小單位,長度不超過32位的資料型別佔用1個Slot,64位的資料型別(long和double)佔用2個Slot