JVM虛擬機器 - Class類檔案結構
JVM虛擬機器 - Class類檔案結構
概述
Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎都是程式執行的必要資料。當遇到需要佔用8位位元組以上空間的資料項時,會按照高位在前的方式分割成若干個8位位元組進行儲存。
Class檔案格式中只有兩種資料型別:無符號數和表。
- 無符號數屬於最基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字串值。
- 表是由多個無符號數或者其他表作為資料項構成的複合資料型別,以“_info”結尾。表用來描述有層次關係的符合結構的資料。
整個Class檔案本質上就是一張表。
無論是無符號數還是表,當需要描述同一型別但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的資料項的形式,稱這一系列連續的某一型別的資料位某一型別的集合。
class檔案的組織結構
- 魔數
- 本檔案的版本資訊
- 常量池
- 訪問標誌
- 類索引
- 父類索引
- 介面索引集合
- 欄位表集合
- 方法表集合
- 屬性表集合
1. 魔數
每個Class檔案的頭4個位元組稱為魔數(Magic Number),唯一作用就是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。
魔數的表示是用16進位制的數:0xCAFEBABE 來表示。
2. 本檔案的版本資訊
在魔數後面的4個位元組儲存的就是Class檔案的版本號:
- 第5、第6個位元組是次版本號(Minor Version)
- 第7、第8個位元組是主版本號(Major Version)
Java版本號的計算
Java的版本號是從45開始的,JDK1.1之後的每個JDK大版本釋出主版本號向上加1。
高版本的JDK能向下相容以前版本的Class檔案,但不能相容以後版本的Class檔案,即使檔案格式並未發生變化,虛擬機器也拒絕執行超過其版本號的Class檔案。
3. 常量池
3.1. 常量池的概念
常量池可以理解為Class檔案之中的資源倉庫,他是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一,同時它還是在Class檔案中第一個出現的表型別資料專案。
常量池中主要存放兩大類常量:
- 字面量:接近於Java語言層面的常量概念,如文字字串、宣告為final的常量
- 符號引用:就是我們定義的各種名字,包括下面三類:
- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
3.2. 常量池的特點
-
常量池長度不固定
常量池的大小是不固定的,因此常量池開頭放置一個u2型別的無符號數,用來儲存當前常量池的容量。JVM根據這個值就知道常量池的頭尾來。
這個無符號數從1開始,不是通常的從0開始
-
常量池中的常量由二維表來表示
常量池開頭有個常量池容器計數器,接下來就全是一個個常量了,只不過常量都是由一張張二維表構成,除了記錄常量的值以外,還記錄當前常量的相關資訊。
-
Class檔案之中的資源倉庫
-
Class檔案結構中與其他專案關聯最多的資料型別
-
佔用Class檔案空間最大的資料專案之一
3.3. 常量池中常量的型別
剛才介紹了,常量池中的常量大體上分為:字面值常量 和 符號引用。在此基礎上,根據常量的資料型別不同,又可以被細分為14種常量型別。這14種常量型別都有各自的二維表示結構。每種常量型別的頭1個位元組都是tag,用於表示當前常量屬於14種類型中的哪一個。
以CONSTANT_Class_info常量為例,它的二維表示結構如下:
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 |
CONSTANT_Utf8_info表示字串常量;
tag表示當前常量的型別,這裡應該是1;
length表示這個字串的長度;
bytes為這個字串的內容(採用縮略的UTF8編碼)
4. 訪問標誌
訪問標誌(access_flags)佔用兩個位元組,這個標誌用於識別一些類或者介面層次的訪問資訊:
- 這個Class是類還是介面
- 是否定位為public型別
- 是否定義為abstract型別
- 是否被聲名為final
- …
訪問標誌中一共有16個標誌位可用,當前只用了8個,其他要求一律為0。
5. 類索引、父類索引和介面索引集合
類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,介面索引集合(interfaces)是一組u2型別的資料的集合。
Class檔案由這三項資料來確定這個類的繼承關係:
-
類索引確定這個類的全限定名
-
父類索引用於確定這個類的父類的全限定名
除
java.lang.Object
外,所有Java類的父類索引都不為0。 -
介面索引集合描述這個類實現了哪些介面
被實現的介面按介面順序從左到右排列在介面索引集合中。
類索引、父類索引和介面索引集合都按順序排列在訪問標誌之後,類索引和父類索引用兩個u2型別的所引致指向一個型別為CONSTANT_Class_info的類描述符常量,該常量的bytes欄位記錄了本類、父類的全限定名。
由於一個類的介面可能有好多個,因此需要用一個集合來表示介面索引,它在類索引和父類索引之後。這個集合頭兩個位元組表示介面索引集合的長度,接下來就是介面的名字索引。
6. 欄位表集合
欄位表用於描述介面或者類中生命的變數。欄位包括類級變數以及例項級變數,但不包括在方法內部宣告的區域性變數。
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
-
access_flags
欄位的訪問標誌。
-
name_index
本欄位名稱的索引,指向一個CONSTANT_Class_info型別的常量,儲存了本欄位的名字等資訊。
-
descriptor_index
欄位和方法的描述符,用於描述本欄位在Java中的資料型別等資訊。
-
attributes_count
屬性表集合的長度。
-
attributes
屬性表集合。
全限定名
全限定名就是把類全名中的.
替換成了/
而已,為了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個;
來表示全限定名結束。
簡單名稱
簡單名稱就是指沒有型別和引數修飾的方法或者欄位名稱。
描述符
描述符的作用是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序)和返回值。
在描述符中:
- 基本資料型別以及代表無返回值的void型別都用一個大寫字元來表示。
- 物件型別則用字元L加物件的全限定名來表示。
標識字元 | 含義 | 標識字元 | 含義 |
---|---|---|---|
B | 基本型別byte | J | 基本型別long |
C | 基本型別char | S | 基本型別short |
D | 基本型別double | Z | 基本型別boolean |
F | 基本型別float | V | 特殊型別void |
I | 基本型別int | L | 物件型別,如Ljava/lang/Object |
-
對於陣列型別,每一唯獨將使用一個前置的“[”字元來描述,如一個定義為
java.lang.String[][]
型別的二維陣列,將被記錄為:[[Ljava/lang/String]]
,一個整形陣列int[]
將被記錄為:[I
。 -
對於描述方法,按照“先引數列表,後返回值的順序描述”,引數列表按照引數的嚴格順序放在一組小括號“()”內。例如:
void inc()
的描述符為:()V
。方法java.lang.String.toString()
的描述符為:()Ljava/lang/String
。方法
int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)
的描述符為:([CII[CIII)I
。
7. 方法表集合
在class檔案中,所有的方法以二維表的形式儲存,每張表來表示一個函式,一個類中的所有方法構成方法表的集合。
方法表的結構和欄位表的結構一致,只不過訪問標誌和屬性表集合的可選項有所不同。
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法中的Java程式碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性裡面,屬性表作為Class檔案格式中最具拓展性的一種資料專案,在第8節中講。
8. 屬性表集合
在Class檔案、欄位表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。
屬性表集合對於各個屬性表的順序不再做嚴格要求,只要不與已有屬性名重複即可。任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java虛擬機器執行時會忽略掉它不認識的屬性。
為了正確解析Class檔案,虛擬機器與定義了一些虛擬機器實現應當能識別的屬性,對於這些屬性,它的名稱需要從常量池中引用一個CONSTANT_Utf9_info型別的常量來表示,而屬性值的結構則是完全自定義的。
一個符合規則的屬性表應該有如下結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |