JVM學(xué)習(xí)筆記(二)——Class文件結(jié)構(gòu)

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可以拿到泛型類型。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 字節(jié)碼查看工具:WinHex 前言 Java虛擬機(jī)實(shí)現(xiàn)語言無關(guān)性的基石就是Class文件Java虛擬機(jī)提供的語言無...
    zlcook閱讀 7,264評(píng)論 4 18
  • 晚秋只道意綿綿 枯葉點(diǎn)點(diǎn) 夢(mèng)里回轉(zhuǎn) 他鄉(xiāng)更比凄零雨 卻孰知 茶香徐徐映星辰
    蘭閣雨湘閱讀 206評(píng)論 4 4
  • “人們數(shù)不清她的屋頂上有多少輪皎潔的明月, 也數(shù)不清她的墻壁之后那一千個(gè)燦爛的太陽?!? ...
    心有靈汐詞為夢(mèng)閱讀 497評(píng)論 0 4
  • Node.js 安裝配置本章節(jié)我們將向大家介紹在windows上安裝Node.js的方法。本安裝教程以Node.j...
    pwld閱讀 197評(píng)論 2 3

友情鏈接更多精彩內(nèi)容