天天看點

虛拟機Class檔案結構筆記

java是與平台無關的語言,“一次編寫,到處運作”,

這一方面依賴于java源代碼編譯後生成的存儲位元組碼的檔案,即class檔案是語言和平台無關的;

另一方面依賴于java虛拟機的實作。

java虛拟機并不關心class的來源是什麼語言,隻要它符合一定的結構,就可以在java中運作。

java語言中的各種變量、關鍵字和運算符的語義最終都是由多條位元組碼指令組合而成的,

是以位元組碼指令所能提供的語義描述能力肯定會比java語言本身更強大,

這便為其他語言實作一些有别于java的語言特性提供了基礎,而且這也正是在類加載時要進行安全驗證的原因。

class檔案是一種8位位元組的二進制流檔案, 各個資料項按順序緊密的從前向後排列, 相鄰的項之間沒有間隙, 這樣可以使得class檔案非常緊湊, 體積輕巧, 可以被jvm快速的加載至記憶體, 并且占據較少的記憶體空間。

我們的java源檔案, 在被編譯之後, 每個類(或者接口)都單獨占據一個class檔案, 并且類中的所有資訊都會在class檔案中有相應的描述, 由于class檔案很靈活, 它甚至比java源檔案有着更強的描述能力。

class檔案中存在以下資料項:

類型

名稱

數量

u4

magic

1

u2

minor_version

major_version

constant_pool_count

cp_info

constant_pool

constant_pool_count - 1

access_flags

this_class

super_class

interfaces_count

interfaces

fields_count

field_info

fields

methods_count

method_info

methods

attribute_count

attribute_info

attributes

attributes_count

 無論是無符号數還是表,當需要描述同一類型但數量不定的多個資料時,經常會在其前面使用一個前置的容量計數器來記錄其數量,而便跟着若幹個連續的資料項,稱這一系列連續的某一類型的資料為某一類型的集合,如:fields_count個field_info表資料便組成了方法表集合。

(1)魔數和版本号 magic minor_version&major_version

每個class檔案的頭4個位元組稱為魔數(magic),它的唯一作用是判斷該檔案是否為一個能被虛拟機接受的class檔案。它的值固定為0xcafebabe。

緊接着magic的4個位元組存儲的是class檔案的次版本号和主版本号,一般情況下, 高版本的jvm能識别低版本的javac編譯器編譯的class檔案, 而低版本的jvm不能識别高版本的javac編譯器編譯的class檔案。 如果使用低版本的jvm執行高版本的class檔案, jvm會抛出java.lang.unsupportedclassversionerror 。

(2)常量池計數和常量池 constant_pool_count&constant_pool

常量池是class檔案中的一項非常重要的資料。 常量池中存放了文字字元串, 常量值, 目前類的類名, 字段名, 方法名, 各個字段和方法的描述符, 對目前類的字段和方法的引用資訊, 目前類中對其他類的引用資訊等等。

常量池中主要存放兩大類常量:字面量和符号引用。字面量比較接近于java層面的常量概念,如文本字元串、被聲明為final的常量值等。而符号引用總結起來則包括了下面三類常量:

類和接口的全限定名(即帶有包名的class名)

字段的名稱和描述符(private、static等描述符)

方法的名稱和描述符(private、static等描述符)

(3) 通路标志 access_flag

在常量池結束之後,緊接着的2個位元組代表通路标志(access_flag),這個标志用于識别一些類或接口層次的通路資訊,包括:這個class是類還是接口,是否定義為public類型,abstract類型,如果是類的話,是否聲明為final,等等。每種通路資訊都由一個十六進制的标志值表示,如果同時具有多種通路資訊,則得到的标志值為這幾種通路資訊的标志值的邏輯或。

(4) 類索引和父類索引 接口索引集合 this_class、super_class、interfaces

類索引(this_class)和父類索引(super_class)都是一個u2類型的資料,而接口索引集合(interfaces)則是一組u2類型的資料集合,class檔案中由這三項資料來确定這個類的繼承關系。類索引、父類索引和接口索引集合都按照順序排列在通路标志之後,類索引和父類索引兩個u2類型的索引值表示,它們各自指向一個類型為comnstant_class_info的類描述符常量,通過該常量中的索引值找到定義在comnstant_utf8_info類型的常量中的全限定名字元串。而接口索引集合就用來描述這個類實作了哪些接口,這些被實作的接口将按implements語句(如果這個類本身是個接口,則應當是extend語句)後的接口順序從左到右排列在接口的索引集合中。

(5)字段表 fields

字段表(field_info)用于描述接口或類中聲明的變量。字段包括了類級變量或執行個體級變量,但不包括在方法内聲明的變量。字段的名字、資料類型、修飾符等都是無法固定的,隻能引用常量池中的常量來描述。

(6)方法表 methods

方法表(method_info)的結構與屬性表的結構相同,方法裡的java代碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“code”的屬性裡。

與字段表集合相對應,如果父類方法在子類中沒有被覆寫,方法表集合中就不會出現來自父類的方法資訊。但同樣,有可能會出現由編譯器自動添加的方法,最典型的便是類構造器“<clinit>”方法和執行個體構造器“<init>”方法。

在java語言中,要重載一個方法,除了要與原方法具有相同的簡單名稱外,還要求必須擁有一個與原方法不同的特征簽名,特征簽名就是一個方法中各個參數在常量池中的字段符号引用的集合,也就是因為傳回值不會包含在特征簽名之中,是以java語言裡無法僅僅依靠傳回值的不同來對一個已有方法進行重載。

(7)屬性表 attributes

屬性表(attribute_info)在前面已經出現過多系,在class檔案、字段表、方法表中都可以攜帶自己的屬性表集合,以用于描述某些場景專有的資訊。

static修飾的字段在類加載過程中的準備階段被初始化為0或null等預設值,而後在初始化階段(觸發類構造器<clinit>)才會被賦予代碼中設定的值,如果沒有設定值,那麼它的值就為預設值。

final修飾的字段在運作時被初始化(可以直接指派,也可以在執行個體構造器中指派),一旦指派便不可更改;

static final修飾的字段在javac時生成constantvalue屬性,在類加載的準備階段根據constantvalue的值為該字段指派,它沒有預設值,必須顯式地指派,否則javac時會報錯。可以了解為在編譯期即把結果放入了常量池中。