JVM-Class文件結構(前篇)

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文件並動態連接時,將會把這些符號應用連接到真正的內存地址入口。

符號引用可表示的內容有:

  1. 類和接口的全限定名。
  2. 字段的名稱和描述符。
  3. 方法的名稱和描述符。

還有一個特殊的常量: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的人。

書山有路勤爲徑。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章