Class文件是Java程序跨平臺(tái)的保證,正是由于有了Class文件架起源碼和機(jī)器碼之間的中間橋梁,JVM虛擬機(jī)才可以在各種平臺(tái)上按照統(tǒng)一的規(guī)范標(biāo)準(zhǔn)加載Java代碼。
作為“寫給虛擬機(jī)看的”Java代碼,Class文件結(jié)構(gòu)必須設(shè)計(jì)得足夠完善,同時(shí)由于Java虛擬機(jī)規(guī)范并不只針對(duì)Java,Class文件又不能引入過多細(xì)節(jié)。本篇博客我們就來介紹下Class文件的結(jié)構(gòu)。
一個(gè)Class文件對(duì)應(yīng)一個(gè)Java Class,所以一個(gè)Class文件記錄著一個(gè)類的全部信息,JVM通過Class文件將對(duì)應(yīng)的類加載入內(nèi)存。
Class文件的結(jié)構(gòu)主要分為以下幾部分:
- 魔數(shù)
- 常量池
- 訪問標(biāo)識(shí)
- 類索引、父類索引、接口索引
- 字段表集合
- 方法表集合
- 索引表集合
1 魔數(shù)
每個(gè)Class文件的頭4個(gè)字節(jié)成為魔數(shù)(Magic Number),它的唯一作用就是確定這個(gè)文件是否能作為一個(gè)Class文件被接受。很多文件都以魔數(shù)進(jìn)行類型識(shí)別,如gif、jpeg等圖片文件。之所以使用魔數(shù)而不是擴(kuò)展名是處于安全考慮,文件擴(kuò)展名可以所以改動(dòng)。Class文件的魔數(shù)是0xCAFEBABE。
緊接著魔數(shù)的4個(gè)字節(jié)存儲(chǔ)的是Class文件的版本號(hào),5、6字節(jié)為次版本號(hào),7、8字節(jié)為主版本號(hào)。不同版本的虛擬機(jī)可以接受不同版本的class文件,所以虛擬機(jī)通過主次版本號(hào)判斷是否可以加載目標(biāo)class文件。
2 常量池
常量池可以看做是Class文件的資源倉(cāng)庫(kù),也是Class文件中占用空間最大的部分。常量池主要存放兩大類常量:字面量、符號(hào)引用。
字面量比較接近Java語言層面的常量,如文本字符串、生命為final的常量等。
符號(hào)引用屬于編譯范疇中的概念,主要包括三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
Java語言不同于C、C++等語言在編譯階段即進(jìn)行鏈接,相應(yīng)的鏈接都放到了運(yùn)行時(shí)階段。所以Class文件中不可能包含各個(gè)方法、字段在內(nèi)存中的布局。Java虛擬機(jī)在運(yùn)行階段加載類時(shí),將符號(hào)引用轉(zhuǎn)換成真正的內(nèi)存入口地址,對(duì)應(yīng)類才算可以工作。
常量池中的每一項(xiàng)代表一個(gè)常量,JDK目前共有14中類型的常量,而每一個(gè)常量又有自己的內(nèi)部結(jié)構(gòu)。類或接口符號(hào)索引是其中較為簡(jiǎn)單的一項(xiàng),接下來以類索引為例做簡(jiǎn)單介紹。類符號(hào)索引對(duì)應(yīng)的類型為CONSTANT_Class_info,其結(jié)構(gòu)如下:
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| u1 | tag | 1 |
| u2 | name_index | 1 |
tag是標(biāo)志位,表明類型。CONSTANT_Class_info的tag為7。name_index是一個(gè)索引值,它指向常量池中一個(gè)CONSTANT_Utf8_info類型常量,此常量代表了這個(gè)類的全限定名。
CONSTANT_Utf8_info的結(jié)構(gòu)如下所示:
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| u1 | tag | 1 |
| u2 | length | 1 |
| u1 | bytes | length |
bytes字段的內(nèi)容就是類的全限定名。
3 訪問標(biāo)志
常量池之后的兩個(gè)字節(jié)代表訪問標(biāo)志(accss_flags),用于識(shí)別類或接口的層次訪問信息:
| 標(biāo)志名稱 | 標(biāo)志值 | 含義 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 是否為public |
| ACC_FINAL | 0x0010 | 是否被聲明為final |
| ACC_SUPER | 0x0020 | 是否允許使用invokespecial字節(jié)碼指令的新語義 |
| ACC_INTERFACE | 0x0200 | 是否為接口 |
| ACCS_ABSTRACT | 0x0400 | 是否為abstract類型 |
| ACC_SYNTHETIC | 0x1000 | 標(biāo)示該類并非由用戶代碼產(chǎn)生 |
| ACC_ANNOTATION | 0x2000 | 標(biāo)示這是一個(gè)注解 |
| ACC_ENUM | 0x4000 | 標(biāo)示這是一個(gè)枚舉 |
4 類索引、父類索引與接口索引集合
類索引(this_class)和父類索引(super_class)都是一個(gè)u2類型的數(shù)據(jù),而接口索引集合(interfaces)是一組u2類型的數(shù)據(jù)集合。Class文件中的這三項(xiàng)決定了類的繼承關(guān)系。
類索引和父類索引用兩個(gè)u2類型的索引值表示,它們各自指向一個(gè)CONSTANT_Class_info類描述符常量,通過CONSTANT_Class_info類型的常量索引值可以找到定義在CONSTAN_Utf8_info類型的常量中的類全限定名。
對(duì)于接口索引集合,入口的第一項(xiàng)u2類型的數(shù)據(jù)為接口計(jì)數(shù)器(interfaces_count)表示索引表的容量。每個(gè)接口的同樣由一個(gè)u2類型數(shù)據(jù)指向一個(gè)CONSTANT_Class_info。
5 字段表集合
字段表(field_info)用于描述接口或者類中聲明的變量。字段(field)包括類級(jí)變量和實(shí)例級(jí)變量,但不包括定義在方法內(nèi)部的局部變量。每個(gè)字段的結(jié)構(gòu)如下圖所示:
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attribute_count |
5.1 訪問標(biāo)識(shí)
| 標(biāo)志名稱 | 含義 |
|---|---|
| ACC_PUBLIC | 是否為public |
| ACC_PRIVATE | 是否為private |
| ACC_PROTECTED | 是否為protected |
| ACC_STATIC | 是否為static |
| ACC_FINAL | 是否為final |
| ACC_VOLATILE | 是否為volatile |
| ACC_TRANSIENT | 是否為transient |
| ACC_SYNTHETIC | 是否為編譯器自動(dòng)產(chǎn)生 |
| ACC_ENUM | 是否為enum |
字段的訪問標(biāo)識(shí)access_flags與類訪問標(biāo)識(shí)類似。
5.2 name_index
name_index標(biāo)識(shí)字段的簡(jiǎn)單名稱。簡(jiǎn)單名稱和全限定名的區(qū)別在于:全限定名是類的全路徑名,如org/fenixsoft/clazz/TestClass,只是把類全名中的"."替換成“/”而已。簡(jiǎn)單名稱指的是沒有類型和參數(shù)修飾的方法或者字段名稱,如一個(gè)類中含有一個(gè)字段"m",則其簡(jiǎn)單名稱為"m"。
5.3 descriptor_index
descriptor_index為字段或方法的描述符。描述符的作用是用來描述字段的數(shù)據(jù)類型、方法的參數(shù)列表(包括數(shù)量、類型以及順序)和返回值?;緮?shù)據(jù)類型以及代表無返回值的void以及對(duì)象類型均由一個(gè)大寫字符來代替:
| 標(biāo)志字段 | 含義 |
|---|---|
| B | byte |
| C | char |
| D | double |
| F | float |
| I | int |
| J | long |
| S | short |
| Z | boolean |
| V | void |
| L | 對(duì)象類型,如Ljava/lang/object |
對(duì)于數(shù)組類型,每一個(gè)維度用一個(gè)"["來描述,比如定義一個(gè)“java.lang.String[][]”類型的二維數(shù)組,將被記錄為“[[Ljava/lang/string”。
方法描述符按照先參數(shù)列表后返回值的順序描述,參數(shù)列表按照參數(shù)順序放在一組"()"之內(nèi)。如方法int indexOf(char[] source, int sourceOffest, int sourceCount, char[] target, int targetOffest, int targetCount, int fromIndex)的描述符為"([CII[CIII)I"。
5.4 attributes_count attribute_info
在描述符之后還有數(shù)量為attributes_count的attribute_info,attribute_info描述字段的額外信息,但這些額外信息最終存放在屬性表中。如“final static int m = 123;”,那就可能會(huì)存在一項(xiàng)名稱為ConstantValue的屬性,其值指向常量123。
6 方法表集合
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attribute_count |
方法表和字段表結(jié)合幾乎一樣,理解了字段表,方法表就非常簡(jiǎn)單了。
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attribute_count |
由于volatile和transient不能修飾方法,所以方法表的訪問標(biāo)識(shí)中沒有了ACC_VOLATILE,ACC_TRANSIENT標(biāo)識(shí)。但同時(shí)又增加了代表synchronized native strictfp abstract的ACC_SYNCHRONIZED ACC_NATIVE ACC_STRICTFP ACC_ABSTRACT。
需要說明的是,方法表集合中并不包含方法里面的代碼。方法代碼經(jīng)過編譯后存放在方法屬性集合中的一個(gè)名為"Code"的屬性里面。例如某方法的屬性表計(jì)數(shù)器attributes_count為1,則表示方法的屬性表集合有一項(xiàng)屬性,屬性索引名稱為0x0009,對(duì)應(yīng)常量為code,說明此屬性是方法的字節(jié)碼描述。
7 屬性表集合
Class文件、字段表、方法表都可以有自己的屬性表,Java7里面定義了21種屬性。
Code屬性
并非所有方法表都有Code屬性,比如接口和抽象類的方法就沒有。結(jié)構(gòu)如下:
| 類型 | 名稱 | 數(shù)量 | 含義 |
|---|---|---|---|
| u2 | attribute_name_index | 1 | 屬性名的索引,對(duì)Code屬性而言恒為”Code” |
| u4 | attribute_length | 1 | 屬性值長(zhǎng)度,相當(dāng)于整個(gè)屬性表長(zhǎng)度長(zhǎng)度減6(u2+u4) |
| u2 | max_stack | 1 | 操作數(shù)棧深度最大值。JVM運(yùn)行時(shí)根據(jù)此值分配棧楨的操作棧深度 |
| u2 | max_locals | 1 | 局部變量表所需存儲(chǔ)空間,單位是Slot,double和long占用2個(gè)Slot、其他基本類型1Slot,Slot空間可以重用(變量作用域問題) |
| u4 | code_length | 1 | 編譯后的字節(jié)碼長(zhǎng)度,理論上最長(zhǎng)2^32-1,實(shí)際上JVM規(guī)定一個(gè)方法不允許超過65535條字節(jié)碼指令 |
| u1 | code | code_length | 代碼編譯后的字節(jié)碼 |
| u2 | exception_table_length | 1 | 異常表長(zhǎng)度 |
| exception_info | exception_table | exception_table_length | 異常表,記錄字節(jié)碼在start_pc到end_pc行之間如果出現(xiàn)類型為catch_type或其子類的異常則跳轉(zhuǎn)到handler_pc行繼續(xù)處理 |
| u2 | attibutes_count | 1 | 屬性表計(jì)數(shù)器 |
| attribute_info | attributes | attibutes_count | 屬性額外描述,比如描述變量初始化值在常量池中的索引 |
字節(jié)碼值得注意的一個(gè)地方是,javac編譯時(shí)將this關(guān)鍵字作為一個(gè)普通方法參數(shù)由JVM調(diào)用時(shí)自動(dòng)傳入。
Exceptions屬性
描述方法可能拋出的受檢異常。
LineNumberTable屬性
描述Java遠(yuǎn)嗎行號(hào)與字節(jié)碼行號(hào)之間映射關(guān)系,也就是為什么拋異常的時(shí)候可以顯示源碼哪一行拋出的。
LocalVariableTable屬性
描述棧楨中局部變量表與Java源碼中變量的關(guān)系,以保證編譯后的代碼被其他代碼調(diào)用時(shí),IDE可以顯示參數(shù)名(否則被arg0、arg1之類的變量名代替)
SourceFile屬性
描述生成當(dāng)前Class文件的源文件名稱,也是拋異常時(shí)可以顯示源文件名字的原因。但內(nèi)部類不會(huì)生成這個(gè)屬性。
ConstantValue屬性
static關(guān)鍵字修飾的變量可以使用這個(gè)屬性。對(duì)于Sun javac編譯器,final static的變量采用ConstantValue屬性初始化,其他static變量在<clinit>(類構(gòu)造器)中初始化。
InnerClasses屬性
記錄內(nèi)部類和宿主類的關(guān)聯(lián)。內(nèi)部類和宿主類的Class文件都會(huì)有這個(gè)屬性。
Signature屬性
記錄泛型簽名信息。Java的泛型是使用擦除式實(shí)現(xiàn)的偽泛型,編譯后擦除泛型,這個(gè)屬性為了彌補(bǔ)此缺陷,方便反射API可以拿到泛型類型。