Java虛擬機器之Class類檔案結構

juexingzhe發表於2018-04-23

歡迎關注微信公眾號: JueCode

正如有一句名言:程式碼編譯的結果從本地機器碼變為位元組碼,是儲存格式發展的一小步,卻是程式語言發展的一大步。 Java語言為什麼能write once, run anywhere? 這個其實是因為和各種不同平臺相關的虛擬機器,這些虛擬機器都可以載入和執行同平臺無關的位元組碼。今天我們就來學習下Class類檔案結構的一些知識。

1.類檔案結構

Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案中,中間沒有新增任何分隔符。Class檔案中只有兩種資料型別:無符號數和表

無符號數屬於基本的資料型別,有u1, u2, u4, u8,分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數。

表則是由多個無符號數或者其他表複合而成的資料型別。所有表都習慣以_info結尾。目前有14個表格型別:

名稱 解釋
CONSTANT_utf8_info utf-8編碼的字串
CONSTANT_Integer_info 整形字面量
CONSTANT_Float_info 浮點型字面量
CONSTANT_Long_info 長整型字面量
CONSTANT_Double_info 雙精度浮點型字面量
CONSTANT_Class_info 類或介面的符號引用
CONSTANT_String_info 字串型別字面量
CONSTANT_Fieldref_info 欄位的符號引用
CONSTANT_Methodref_info 類中方法的符號引用
CONSTANT_Interface_Methodref_info 介面中方法的符號引用
CONSTANT_NameAndType_info 欄位或方法的部分符號引用
CONSTANT_MethodHandle_info 表示方法控制程式碼
CONSTANT_MethodType_info 表示方法型別
CONSTANT_InvokeDynamic_info 表示一個動態方法呼叫點

整個Class檔案是有順序的,整個格式如下面的表格:

型別 名稱 數量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attribute_count 1
attribute_info attributes attribute_count

Class檔案格式都是嚴格按照上面順序,當然有的型別可能沒有,比如一個類沒有實現介面,那麼interfaces_count 的數值就為0,後面的interfaces就沒有,以此類推。

下面我們看一個簡單的栗子來分析Class檔案結構。

package org.fenixsoft.clazz;

public class TestClass{
	private int m;
	public int inc(){
		return m + 1;
	}
}
複製程式碼

通過javac TestClass 可以編譯得到TestClass.class檔案:

cafe babe 0000 0034 0013 0a00 0400 0f09 0003 0010 
0700 1107 0012 0100 016d 0100 0149 0100 063c 696e 
6974 3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 
696e 654e 756d 6265 7254 6162 6c65 0100 0369 6e63 
0100 0328 2949 0100 0a53 6f75 7263 6546 696c 6501 
000e 5465 7374 436c 6173 732e 6a61 7661 0c00 0700 
080c 0005 0006 0100 1d6f 7267 2f66 656e 6978 736f 
6674 2f63 6c61 7a7a 2f54 6573 7443 6c61 7373 0100 
106a 6176 612f 6c61 6e67 2f4f 626a 6563 7400 2100 
0300 0400 0000 0100 0200 0500 0600 0000 0200 0100 
0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a 
b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003 
0001 000b 000c 0001 0009 0000 001f 0002 0001 0000 
0007 2ab4 0002 0460 ac00 0000 0100 0a00 0000 0600 
0100 0000 0600 0100 0d00 0000 0200 0e
複製程式碼

現在看這個十六進位制class檔案肯定一臉懵*,按照格式來劃分:

//TestCalss.class
cafe babe //MagicNumber
0000 //minor_version
0034 //major_version  52 --- jdk 1.8   (50 --- jdk 1.6)
0013 //constant_pool_count 19(從1開始)
0a00 0400 0f09 0003 0010 0700 1107 0012 0100 016d 0100 0149 0100 063c 696e 6974 
3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 
0100 0369 6e63 0100 0328 2949 0100 0a53 6f75 7263 6546 696c 6501 000e 5465 7374 
436c 6173 732e 6a61 7661 0c00 0700 080c 0005 0006 0100 1d6f 7267 2f66 656e 6978 
736f 6674 2f63 6c61 7a7a 2f54 6573 7443 6c61 7373 0100 106a 6176 612f 6c61 6e67 
2f4f 626a 6563 74 //常量池 18個
0021 //access_flags
0003 //this_class
0004 //super_class
0000 //interfaces_count
0001 //fields_count
0002 0005 0006 0000 //fields
0002 //methods_count
0001 0007 0008 0001 0009	//methods
0000001d 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 01 00 0a 00 00 00 06 00 
01 00 00 00 03 0001 000b 000c 0001 0009 0000 001f 0002 0001 0000 0007 2ab4 0002 
0460 ac00 0000 0100 0a00 0000 0600 0100 0000 0600 0100 0d00 0000 0200 0e//Code
複製程式碼

接下來對照著這個十六進位制class檔案和上面的檔案格式來挨個拆解。

2.MagicNumber/version

首先看到前面三個選項,分別是MagicNumber minor_version major_version 其中MagicNumber是固定4個位元組的常量0xcafebabe.

//TestCalss.class
cafe babe //MagicNumber
0000 //minor_version
0034 //major_version  52 --- jdk 1.8   (50 --- jdk 1.6)
複製程式碼

minor_version和major_version描述的是jdk的版本,十六進位制的34轉化為十進位制就是52,也就是對應jdk 1.8版本,50對應的是jdk 1.6版本,一次類推。

緊接著主次版本號之後的是常量池。

3.常量池

常量池可以理解為Class檔案中的資源倉庫,是佔用Class檔案空間最大的資料專案之一。 常量池中常量的數量是不固定的,所以在常量池入口放置一項u2型別的資料代表常量池容易計數值,有個點需要注意這個容量計數是從1而不是0開始。第0項常量空出來是表達“不引用任何一個常量池專案”。 看下我們的栗子, 0x0013即十進位制的19,代表常量池中有18項常量

0013 //constant_pool_count 19(從1開始)
0a00 0400 0f09 0003 0010 0700 1107 0012 0100 016d 0100 0149 0100 063c 696e 6974 
3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 
0100 0369 6e63 0100 0328 2949 0100 0a53 6f75 7263 6546 696c 6501 000e 5465 7374 
436c 6173 732e 6a61 7661 0c00 0700 080c 0005 0006 0100 1d6f 7267 2f66 656e 6978 
736f 6674 2f63 6c61 7a7a 2f54 6573 7443 6c61 7373 0100 106a 6176 612f 6c61 6e67 
2f4f 626a 6563 74 //常量池 
複製程式碼

常量池中主要存放兩大類常量:字面量和符號引用。 字面量接近Java中的常量概念,比如字串,宣告為final的常量值等。 符號引用包括下面三類:

  • 類和介面的全限定名
  • 欄位的名稱和描述符
  • 方法的名稱和描述符

常量池中的每一項常量都是一個表,不同的表是有不同的結構,接下來我們來看看14種表的具體含義:

名稱 標誌 描述
CONSTANT_utf8_info 1 utf-8編碼的字串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮點型字面量
CONSTANT_Long_info 5 長整型字面量
CONSTANT_Double_info 6 雙精度浮點型字面量
CONSTANT_Class_info 7 類或介面的符號引用
CONSTANT_String_info 8 字串型別字面量
CONSTANT_Fieldref_info 9 欄位的符號引用
CONSTANT_Methodref_info 10 類中方法的符號引用
CONSTANT_Interface_Methodref_info 11 介面中方法的符號引用
CONSTANT_NameAndType_info 12 欄位或方法的部分符號引用
CONSTANT_MethodHandle_info 15 表示方法控制程式碼
CONSTANT_MethodType_info 16 表示方法型別
CONSTANT_InvokeDynamic_info 18 表示一個動態方法呼叫點

通過命令

javap -verbose TestClass
複製程式碼

就可以把上面的18個常量都計算出來,省得自己挨個根據ASCII碼進行計算,得到下面的常量表:

常量池//常量池 18個
10a 0004 000f					Methodref #4, #15
209 0003 0010 					Fieldref  #3, #16
307 0011						Class #17
407 0012 						Class #18
501 0001 6d					utf-8 m
601 0001 49					utf-8 I 
701 0006 3c 69 6e 69 74 3e		utf-8 <init>
801 0003 28 29 56				utf-8 ()V
901 0004 43 6f 64 65 			utf-8 Code
1001 000f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 	utf-8 LineNumberTable
1101 0003 69 6e 63 			utf-8 inc
1201 0003 28 29 49 			utf-8 ()I
1301 000a 53 6f 75 72 63 65 46 69 6c 65 		utf-8 SourceFile
1401 000e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 	utf-8 TestClass.java
150c 0007 0008					NameAndType #7:#8
160c 0005 0006 				NameAndType #5:#6
1701 001d 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2f 63 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 73 							 utf-8 org/fenixsoft/clazz/TestClass
1801 0010 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 		utf-8 java/lang/Object
複製程式碼

舉個栗子,比如第三個開頭是07,那麼就是對應CONSTANT_Class_info這個info,而CONSTANT_Class_info對應的是下面的資料結構:

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

那麼緊跟07 後面的11就是索引第11項常量的意思,第11項是01 0003 69 6e 63, 其中tag是01,也就是CONSTANT_utf8_info這個info,它的資料結構:

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

所以,長度是3,往後數三個位元組就是69 6e 63,對應的就是inc,這個也就是方法的名稱,其他的都是這樣的分析方式: 首先找到tag對應的表資料結構,然後根據資料結構拆分。

篇幅所限,其他的常量項的結構可以參考深入理解Java虛擬機器

緊接著常量池後的是訪問標誌。

4.訪問標誌

在常量池之後緊接這兩個位元組是訪問標誌,識別一些類或者介面層次的訪問資訊:

Class是類或者介面 是否public 是否abstract 是否final

具體的標誌位和含義如下面表格:

名稱 標誌值 含義
ACC_PUBLIC 0x0001 是否為public
ACC_FINAL 0x0010 是否為final
ACC_SUPER 0x0020 JDK 1.0.2之後編譯出來的類這個標誌都為真
ACC_INTERFACE 0x0200 是否為一個介面
ACC_ABSTRACT 0x0400 是否為abstract型別
ACC_SYNTHETIC 0x1000 標識這個類並非由使用者程式碼產生
ACC_ANNOTATION 0x2000 是否是註解
ACC_ENUM 0x4000 是否是列舉

在我們這個栗子中類是public 是JDK1.8編譯出來的,所以access_flags的值為:ACC_PUBLIC | ACC_SUPER = 0x0021

5.類索引/父類索引/介面索引

在訪問標誌後分別是this_class/super_class/interfaces_count

0003 //this_class			確定這個類的全限定名
0004 //super_class 		 	java.lang.Object該值就是0000
0000 //interfaces_count		該類沒有實現任何介面,介面的索引表不佔用任何位元組
複製程式碼

有的小夥伴就要急了,上面的0003為什麼代表this_class?其實這個0003就是在常量池中的索引,回顧前面常量池中第3的索引是:07 0011這個是CONSTANT_Class_info的資料結構,指向第17的索引:

01 001d 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2f 63 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 73
複製程式碼

這個是CONSTANT_utf8_info的資料結構,對應就是

org/fenixsoft/clazz/TestClass
複製程式碼

這個就是類的全限定名。

其它兩個的分析以此類推,在這個例子中沒有實現介面,所以介面數量是0,也就沒有後面的interfaces。

緊接著的就是fields_count和fields。

6.欄位表集合

欄位表field_info用於描述類和介面中宣告的變數。變數包括類級變數和例項級變數,但是不包括方法中的變數。描述欄位的資訊都有哪些?有作用域(public/private/protect等),static,欄位名字,欄位資料型別,其中可以用布林型別描述的有:

欄位的作用域,public/private/protected 例項變數還是類變數,static 可變性,final 併發可見性, volatile 可否被序列化, transient

類似與上面的access_flags, 能用布林型別表示的定義下面的標誌位:

名稱 標誌值
ACC_PUBLIC 0x0001
ACC_PRIVATE 0x0002
ACC_PROTECTED 0x0004
ACC_STATIC 0x0008
ACC_FINAL 0x0010
ACC_VOLATILE 0x0040
ACC_TRANSIENT 0x0080
ACC_SYNTHETIC 0x1000
ACC_ENUM 0x4000

不能用布林型別描述的有:

欄位名字 欄位資料型別,基本型別/物件/陣列

欄位名稱肯定是索引常量池中的資料項,欄位資料型別呢?專門定義了描述符來標識資料型別, 物件型別用字元L加物件的全限定名來表示:

標識字元 含義 標識字元 含義
B 基本型別byte J 基本型別long
C 基本型別char S 基本型別short
D 基本型別double Z 基本型別boolean
F 基本型別float V 特殊型別void
I 基本型別int L 物件型別,如L/java/lang/Object

對於陣列型別,每一個維度使用一個前置的“[”字元來描述,如“String[][]”表示為“[[Ljava/lang/String;”

欄位表也有專門的結構, descriptor_index之後可以跟著屬性表集合儲存一些額外的資訊,比如private static int m = 123, 那麼可能會有一項ConstantValue的屬性儲存123這個值。

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

對於我們的例子TestClass, private int m;

//fields_count
0001 
//fields
0002 //private
0005 //m
0006 //I
0000 //attribute_count
複製程式碼

緊跟著欄位表之後的就是方法表集合。

7.方法表集合

方法表集合和欄位表集合很類似,有一個區別就是用描述符描述方法時,需要先引數列表後返回值,比如

void inc()  ------> ()V

java.lang.String toString(int index) ---> (I)Ljava/lang/String
複製程式碼

跟屬性表一樣,方法表也有專門的資料結構:

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

在TestClass中有兩個方法,一個是預設建構函式,一個是方法inc

//methods_count,編譯器新增的例項構造器<init>和原始碼inc()
0002 
//methods
0001 	//public
0007	//<init>
0008 	//()V
0001	        //attribute_count
0009	//Code,存放方法裡面的Java程式碼
......
//methods
0001      //public 
000b     //inc
000c     //()I
0001     //attribute_count
//Atrribute
//Code
0009    //Code,存放方法裡面的Java程式碼
複製程式碼

其中Code是方法的屬性,用於存放方法的Java程式碼編譯成的位元組碼指令。

最後一個格式就是屬性表集合了。

8.屬性表集合

虛擬機器規範預定義的屬性有21項,這裡簡單看下常用的幾項:

屬性 使用位置 含義
Code 方法表 Java程式碼編譯成的位元組碼指令
ConstantValue 欄位表 final關鍵字定義的常量值
LineNumberTable Code屬性 Java原始碼的行號與位元組碼指令的對應關係
SourceFile 類檔案 記錄原始檔名稱

屬性表結構

型別 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

其中Code屬性表的結構, attribute_name_index是指向常量池的索引,這裡就是'Code'.

型別 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table 1
u2 attributes_count 1
attribute_info attributes attributes_count

在我們例子中就是:

//Atrribute
0009 //attribute_name_index--->Code	
0000001d //attribute_length--->29
0001 	//max_stack 運算元棧
0001 	//max_locals	區域性變數表需要的儲存空間 單位slot
00000005 //code_length 位元組碼長度
2a b7 00 01 b1 //code 儲存位元組碼指令的一序列位元組流
0000 //exception_table_length
0001 //attributes_count--->Code的屬性
//LineNumberTable描述Java原始碼行號與位元組碼行號之間的對應關係
000a //attribute_name_index
00000006 	//attribute_length
0001 		//line_number_table_length
0000 		//start_pc 位元組碼行號
0003 		//Java原始碼行號

//method
0001      //public 
000b     //inc
000c     //()I
0001     //attribute_count
//Atrribute
//Code
0009    //Code,存放方法裡面的Java程式碼
0009 
0000001f 
0002 
0001 
00000007 
2a b4 00 02 04 60 ac	//code 儲存位元組碼指令的一序列位元組流
0000
0001
//LineNumberTable
000a
00000006
0001
0000
0006
0001
//SourceFile
000d		//SourceFile
00000002
000e
複製程式碼

9.總結

能讀懂Class類檔案結構是理解虛擬機器的入門功課,本次分享從一個簡單例子詳細闡述了類檔案的結構格式,有一些細節沒有仔細說明,比如屬性表的另外的屬性,還有常量池中資料項,屬性表中異常表。但是有了上面的知識儲備,自行分析剩下的就不是什麼問題了。

另外,本文的思路和例子也是參考深入理解Java虛擬機器: JVM高階特性與最佳實踐這本書,很經典,建議小夥伴們可以看看。

謝謝大家!

相關文章