JVM Class文件結構
什麼是Class文件?
Class文件是一組以8位字節(即8bit的字節,可表示爲u1)爲單位的二進制流,各個數據項嚴格按順序緊密排布在文件中,中間沒有分隔符,當遇到需要一個8位字節以上空間的數據項時,會按高位在前的方式分割成若干個8位字節進行存儲。
如果把Class轉爲16進制數表示,就一定有2n個16進制數。
Class的數據結構:
1、無符號數
2、表
無符號數相當於基本數據類型,根據各類型佔用字節數不同,可分別用u1、u2、u4、u8來表示一個字節、兩個字節、四個字節、八個字節的無符號數,可表示數值、索引、utf-8編碼的字符串等。
表可由無符號數和表組成,一般以“_info”結尾,Class文件本質是一張表。
當描述同一類型但數量不定但數據時,會採用前置計數器後接連續數據項的形式,如描述含有n項常量的常量池,先指定一個計數器值爲n,後面連續n項爲常量池包含的常量。
接下來就開始正式探索Class文件結構了
首先創建ClassTest.java,內容如下:
public class ClassTest {
private int param;
private static final String STATIC_PARAM = "static_param";
public void function(){
}
public void function(int funParam){
param = funParam;
}
public static void main(String[] args) {
System.out.println(STATIC_PARAM);
}
}
javac編譯後得到class文件,內容如下
1、魔數
Class的前四個字節爲魔數,用以判斷該文件是否是能被虛擬機接受的Class文件,文件後綴可任意更改並不可靠,Class文件的魔數爲0xCAFEBABE。
對比得到的class文件,開頭確實是cafebabe,接下來將cafebabe改爲cafebaba
依次執行:
javac ClassTest.java
java ClassTest
結果如下
2、版本號
這四個字節代表了jdk版本號,5、6字節(0000)爲從版本號,7、8字節(0035)爲主版本號,
java版本號從45開始,jdk 1.x的主版本號爲45,以後每個大版本都會在主版本號上加1。
驗證一下,0035 即爲十進制的53,53 - 44 = 9,即jdk 9.x(1.9)
java -version結果如下
嗯。。很穩,根據java向下兼容的特性,筆者使用的jdk用於任何主版本號小於等於53的class文件。
3、常量池
從第九字節起爲常量池,它包含了一個長度爲u2、表示有n(0~2^16)項常量表的數據,和緊隨其後的n項常量表。
常量表有多種類型,每種類型都有相應的結構,但所有的常量表都以“tag + 具體數據” 的方式組成,不同的類型通過常量表首位的tag區分,常量表大體上可分爲表示字面量的常量和表示符號引用的常量。
字面量是指常量所表示的內容就是實際內容。
字面量可表示的內容有:整型常量、浮點型常量、長整型常量、雙精度浮點型常量、字符串常量。
符號引用是指常量所表示的內容是一個具體內容的標識,比如類的全限定名可以作爲一個類的標識。JVM當加載Class文件並動態連接時,將會把這些符號應用連接到真正的內存地址入口。
符號引用可表示的內容有:
- 類和接口的全限定名。
- 字段的名稱和描述符。
- 方法的名稱和描述符。
還有一個特殊的常量:utf-8字符串,它一般用來表示符號引用本身。
jdk1.7之後又加入了表示方法句柄的常量項、表示標識方法的常量項、表示動態方法調用點的常量項。
查看實例Class文件的第9、10字節,表示的數爲十進制的38,所以接下來的37項爲常量項,爲什麼不是38項呢?因爲第0項被設計者空出來的,從1開始計數,即1~37,共37項(如果0沒有被空出來,則爲0~37共38項)。常量池索引爲1,表示引用常量池第一項;常量池索引爲0,表示不引用常量池項目。
第11字節是0A,它第一項常量的tag,對照項目類型可以看出tag爲10的常量類型爲方法符號引用,查詢該常量表的結構可知,該常量表由u1的tag,u2的class描述符索引和u2的NameAndType索引組成。
查看接下來的兩個u2類型索引:00 07,0017。分別指向第7項和第23項。
常量項太多了,所以直接使用javap -verbose /classpath 導出字節碼內容,內容如下
查看第7項和第23項,確實是Class類型和NameAndType類型的常量項。分別代表了Object類、<init>方法及其參數和返回值。
照此往下分析,我們可以知道常量池的組成,進而根據JVM的規定實現自己的常量池解析器。
4、訪問標誌及繼承、實現關係
至此,我們以分析完常量池內容,緊接着常量池的是訪問標誌,即從畫紅線的一項起爲接下來要分析的內容。
訪問標誌是一個長度u2的數據項,它指明瞭類或接口層次的訪問信息。
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否修飾爲public |
ACC_FINAL | 0x0010 | 是否修飾爲final |
ACC_SUPER | 0x0020 | 是否啓用新語意,jdk 1.0.2之後必須爲真 |
ACC_INTERFACE | 0x0200 | 是否爲接口 |
ACC_ABSTRACT | 0x0400 | 是否是抽象的;接口和抽象類爲真,其他類值爲假 |
ACC_SYNTHETIC | 0x1000 | 是否由非用戶代碼產生 |
ACC_ANNOTATION | 0x2000 | 是否爲註解 |
ACC_ENUM | 0x4000 | 是否爲枚舉 |
對照該表查看標誌信息,0021,即由ACC_SUPER|ACC_PUBLIC(0x0020|0x0001)組成,所以該Class是一個啓用了新語意的public類。
接下來的三項u2數據分別表示了當前類索引、父類索引、接口索引集合容量。
0004指向常量池第4項,查看可以得知是一個類(或接口)的符號引用,它指向第27項,表示一個全限定名,查看第27項,確實是當前類的類名ClassTest。
0007指向常量池第7項,查看第7項,也是一個類(或接口)的符號引用,它指向第31項,查看第31項,是我們熟悉的Object類,也就是ClassTest的父類。
0000是接口索引集合容量,該容量爲0,就是說沒有實現任何接口。
5、字段表集合
接着是字段表集合,首先有一個u2類型的數據項表示集合容量n,接下來的n項爲字段項。可以看到容量爲0x0002,集合中包含兩個字段項。
字段表包含訪問標示、字段名、描述符、屬性數目及屬性項等。
1.訪問標識(u2)
字段訪問修飾符包括public/protected/private、static、final/volatile、transient、enum
表示值及含義見下表
標誌名稱 | 標誌值 | 含義 |
---|---|---|
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 | 是否爲enum |
第一個字段的訪問標示爲0002,即訪問類型爲private。
2.字段名(u2)
字段名是字段的簡單名稱,第一個字段的字段名索引爲0008,指向常量池第8項,查看常量池第8項可知,該字段名爲param。
3.描述符(u2)
描述符表示了該字段的數據類型,方法的描述符表示了返回類型、參數列表。
描述符標識字符見下表
標識字符 | 含義 |
---|---|
B | 基本類型byte |
C | 基本類型Char |
D | 基本類型double |
F | 基本類型float |
I | 基本類型int |
J | 基本類型long |
S | 基本類型short |
V | 特殊類型void |
Z | 基本類型boolean |
L | 對象類型,如Ljava/lang/Object |
int類型可用I表示,double可用D表示,String類型可用Ljava/lang/String表示,String數組可用[Ljava/lang/String表示,二維String可用[[Ljava/lang/String表示。
查看第一個字段的標識符,0008,指向了常量池的第9項,查看第9項,是I,與我們定義的int param類型一致。
4.屬性數目(u2)
5.屬性表集合
屬性的內容較多,即可以放置final標識的字段常量值,也可以放置方法的字節碼指令等,內容較爲複雜,將在下一篇文章介紹,先來驗證一下已學內容。
第一個字段的屬性數目爲0000,也就是說它沒有屬性。
我們先來看定義的第二個字段
private static final String STATIC_PARAM = "static_param";
訪問標識爲private static final,查閱訪問標識表,可以算出第二個字段的訪問標識爲0x0002 | 0x0008 | 0x0010 = 0x001A
查看常量池,值爲STATIC_PARAM的常量項索引值爲0x000A
String的描述標識符爲Ljava/lang/String,在常量池中查找該項,索引值爲0x000B
由此可得,接下來的6個字節依次爲001A 000A 000B,查看Class文件,確實與我們所算出的值一致。
完美~
6、方法表集合
方法表與字段表如出一轍,先是一個u2類型的數據項表示方法表集合容量n,緊接着的n項爲方法項,方法表的結構與字段表如出一轍,接下來就按照字段表的學習方法表。
1.訪問標誌(u2)
方法訪問修飾符包括public/private/protected、static、final、synchronized、native、strictfp、abstract。
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否爲public |
ACC_PRIVETE | 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 | 是否爲abstract |
ACC_STRICTFP | 0x0800 | 是否爲strictfp(精確運算浮點型) |
ACC_SYNTHETIC | 0x1000 | 是否爲非用戶代碼產生的方法 |
2.方法名(u2)
方法名和字段名一樣,指向一個常量池索引,在常量池中是一個utf8類型的常量項。
3.描述符(u2)
方法描述符和字段描述符類似,都使用規定的標識字符代表對應基本數據類型和對象類型,但方法描述符比字段描述符稍複雜一些,方法描述符同時指明瞭參數列表和返回類型,參數列表和返回類型都使用標識字符表示,格式爲(參數列表)返回類型。
例如,public int[] getArray(String[] strs);
參數爲String[] strs,轉換爲標識字符->[Ljava/lang/String
返回類型爲int[],轉換爲標識字符->[I
所以public int[] getArray(String[] strs);用描述符標識爲([I)[Ljava/lang/String;
方法描述符在JNI中也被稱作方法簽名。
屬性數目和屬性表集合在下一篇文章學習。
《JVM》系列的文章思路沿襲《深入理解Java虛擬機》周志明版,作爲本人學習《深入理解Java虛擬機》一書的學習筆記,希望它也能幫到其他在學習JVM的人。
書山有路勤爲徑。