JVM學習筆記(二)——Class文件結構

Class文件是Java程序跨平臺的保證,正是由於有了Class文件架起源碼和機器碼之間的中間橋樑,JVM虛擬機纔可以在各種平臺上按照統一的規範標準加載Java代碼。

作爲“寫給虛擬機看的”Java代碼,Class文件結構必須設計得足夠完善,同時由於Java虛擬機規範並不只針對Java,Class文件又不能引入過多細節。本篇博客我們就來介紹下Class文件的結構。

一個Class文件對應一個Java Class,所以一個Class文件記錄着一個類的全部信息,JVM通過Class文件將對應的類加載入內存。

Class文件的結構主要分爲以下幾部分:

  • 魔數
  • 常量池
  • 訪問標識
  • 類索引、父類索引、接口索引
  • 字段表集合
  • 方法表集合
  • 索引表集合

1 魔數

每個Class文件的頭4個字節成爲魔數(Magic Number),它的唯一作用就是確定這個文件是否能作爲一個Class文件被接受。很多文件都以魔數進行類型識別,如gif、jpeg等圖片文件。之所以使用魔數而不是擴展名是處於安全考慮,文件擴展名可以所以改動。Class文件的魔數是0xCAFEBABE。

緊接着魔數的4個字節存儲的是Class文件的版本號,5、6字節爲次版本號,7、8字節爲主版本號。不同版本的虛擬機可以接受不同版本的class文件,所以虛擬機通過主次版本號判斷是否可以加載目標class文件。

2 常量池

常量池可以看做是Class文件的資源倉庫,也是Class文件中佔用空間最大的部分。常量池主要存放兩大類常量:字面量、符號引用。

字面量比較接近Java語言層面的常量,如文本字符串、生命爲final的常量等。

符號引用屬於編譯範疇中的概念,主要包括三類常量:

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

Java語言不同於C、C++等語言在編譯階段即進行鏈接,相應的鏈接都放到了運行時階段。所以Class文件中不可能包含各個方法、字段在內存中的佈局。Java虛擬機在運行階段加載類時,將符號引用轉換成真正的內存入口地址,對應類纔算可以工作。

常量池中的每一項代表一個常量,JDK目前共有14中類型的常量,而每一個常量又有自己的內部結構。類或接口符號索引是其中較爲簡單的一項,接下來以類索引爲例做簡單介紹。類符號索引對應的類型爲CONSTANT_Class_info,其結構如下:

類型 名稱 數量
u1 tag 1
u2 name_index 1

tag是標誌位,表明類型。CONSTANT_Class_info的tag爲7。name_index是一個索引值,它指向常量池中一個CONSTANT_Utf8_info類型常量,此常量代表了這個類的全限定名。

CONSTANT_Utf8_info的結構如下所示:

類型 名稱 數量
u1 tag 1
u2 length 1
u1 bytes length

bytes字段的內容就是類的全限定名。

3 訪問標誌

常量池之後的兩個字節代表訪問標誌(accss_flags),用於識別類或接口的層次訪問信息:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 是否爲public
ACC_FINAL 0x0010 是否被聲明爲final
ACC_SUPER 0x0020 是否允許使用invokespecial字節碼指令的新語義
ACC_INTERFACE 0x0200 是否爲接口
ACCS_ABSTRACT 0x0400 是否爲abstract類型
ACC_SYNTHETIC 0x1000 標示該類並非由用戶代碼產生
ACC_ANNOTATION 0x2000 標示這是一個註解
ACC_ENUM 0x4000 標示這是一個枚舉

4 類索引、父類索引與接口索引集合

類索引(this_class)和父類索引(super_class)都是一個u2類型的數據,而接口索引集合(interfaces)是一組u2類型的數據集合。Class文件中的這三項決定了類的繼承關係。

類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個CONSTANT_Class_info類描述符常量,通過CONSTANT_Class_info類型的常量索引值可以找到定義在CONSTAN_Utf8_info類型的常量中的類全限定名。

對於接口索引集合,入口的第一項u2類型的數據爲接口計數器(interfaces_count)表示索引表的容量。每個接口的同樣由一個u2類型數據指向一個CONSTANT_Class_info。

5 字段表集合

字段表(field_info)用於描述接口或者類中聲明的變量。字段(field)包括類級變量和實例級變量,但不包括定義在方法內部的局部變量。每個字段的結構如下圖所示:

類型 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attribute_count

5.1 訪問標識

標誌名稱 含義
ACC_PUBLIC 是否爲public
ACC_PRIVATE 是否爲private
ACC_PROTECTED 是否爲protected
ACC_STATIC 是否爲static
ACC_FINAL 是否爲final
ACC_VOLATILE 是否爲volatile
ACC_TRANSIENT 是否爲transient
ACC_SYNTHETIC 是否爲編譯器自動產生
ACC_ENUM 是否爲enum

字段的訪問標識access_flags與類訪問標識類似。

5.2 name_index

name_index標識字段的簡單名稱。簡單名稱和全限定名的區別在於:全限定名是類的全路徑名,如org/fenixsoft/clazz/TestClass,只是把類全名中的”.”替換成“/”而已。簡單名稱指的是沒有類型和參數修飾的方法或者字段名稱,如一個類中含有一個字段”m”,則其簡單名稱爲”m”。

5.3 descriptor_index

descriptor_index爲字段或方法的描述符。描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。基本數據類型以及代表無返回值的void以及對象類型均由一個大寫字符來代替:

標誌字段 含義
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 對象類型,如Ljava/lang/object

對於數組類型,每一個維度用一個”[“來描述,比如定義一個“java.lang.String[][]”類型的二維數組,將被記錄爲“[[Ljava/lang/string”。

方法描述符按照先參數列表後返回值的順序描述,參數列表按照參數順序放在一組”()”之內。如方法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

在描述符之後還有數量爲attributes_count的attribute_info,attribute_info描述字段的額外信息,但這些額外信息最終存放在屬性表中。如“final static int m = 123;”,那就可能會存在一項名稱爲ConstantValue的屬性,其值指向常量123。

6 方法表集合

方法表和字段表結合幾乎一樣,理解了字段表,方法表就非常簡單了。

類型 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attribute_count

由於volatile和transient不能修飾方法,所以方法表的訪問標識中沒有了ACC_VOLATILE,ACC_TRANSIENT標識。但同時又增加了代表synchronized native strictfp abstract的ACC_SYNCHRONIZED ACC_NATIVE ACC_STRICTFP ACC_ABSTRACT。

需要說明的是,方法表集合中並不包含方法裏面的代碼。方法代碼經過編譯後存放在方法屬性集合中的一個名爲”Code”的屬性裏面。例如某方法的屬性表計數器attributes_count爲1,則表示方法的屬性表集合有一項屬性,屬性索引名稱爲0x0009,對應常量爲code,說明此屬性是方法的字節碼描述。

7 屬性表集合

Class文件、字段表、方法表都可以有自己的屬性表,Java7裏面定義了21種屬性。

Code屬性

並非所有方法表都有Code屬性,比如接口和抽象類的方法就沒有。結構如下:

類型 名稱 數量 含義
u2 attribute_name_index 1 屬性名的索引,對Code屬性而言恆爲”Code”
u4 attribute_length 1 屬性值長度,相當於整個屬性表長度長度減6(u2+u4)
u2 max_stack 1 操作數棧深度最大值。JVM運行時根據此值分配棧楨的操作棧深度
u2 max_locals 1 局部變量表所需存儲空間,單位是Slot,double和long佔用2個Slot、其他基本類型1Slot,Slot空間可以重用(變量作用域問題)
u4 code_length 1 編譯後的字節碼長度,理論上最長2^32-1,實際上JVM規定一個方法不允許超過65535條字節碼指令
u1 code code_length 代碼編譯後的字節碼
u2 exception_table_length 1 異常表長度
exception_info exception_table exception_table_length 異常表,記錄字節碼在start_pc到end_pc行之間如果出現類型爲catch_type或其子類的異常則跳轉到handler_pc行繼續處理
u2 attibutes_count 1 屬性表計數器
attribute_info attributes attibutes_count 屬性額外描述,比如描述變量初始化值在常量池中的索引

字節碼值得注意的一個地方是,javac編譯時將this關鍵字作爲一個普通方法參數由JVM調用時自動傳入。

Exceptions屬性

描述方法可能拋出的受檢異常。

LineNumberTable屬性

描述Java遠嗎行號與字節碼行號之間映射關係,也就是爲什麼拋異常的時候可以顯示源碼哪一行拋出的。

LocalVariableTable屬性

描述棧楨中局部變量表與Java源碼中變量的關係,以保證編譯後的代碼被其他代碼調用時,IDE可以顯示參數名(否則被arg0、arg1之類的變量名代替)

SourceFile屬性

描述生成當前Class文件的源文件名稱,也是拋異常時可以顯示源文件名字的原因。但內部類不會生成這個屬性。

ConstantValue屬性

static關鍵字修飾的變量可以使用這個屬性。對於Sun javac編譯器,final static的變量採用ConstantValue屬性初始化,其他static變量在(類構造器)中初始化。

InnerClasses屬性

記錄內部類和宿主類的關聯。內部類和宿主類的Class文件都會有這個屬性。

Signature屬性

記錄泛型簽名信息。Java的泛型是使用擦除式實現的僞泛型,編譯後擦除泛型,這個屬性爲了彌補此缺陷,方便反射API可以拿到泛型類型。

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