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