歡迎關注微信公眾號: 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個
1、0a 0004 000f Methodref #4, #15
2、09 0003 0010 Fieldref #3, #16
3、07 0011 Class #17
4、07 0012 Class #18
5、01 0001 6d utf-8 m
6、01 0001 49 utf-8 I
7、01 0006 3c 69 6e 69 74 3e utf-8 <init>
8、01 0003 28 29 56 utf-8 ()V
9、01 0004 43 6f 64 65 utf-8 Code
10、01 000f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 utf-8 LineNumberTable
11、01 0003 69 6e 63 utf-8 inc
12、01 0003 28 29 49 utf-8 ()I
13、01 000a 53 6f 75 72 63 65 46 69 6c 65 utf-8 SourceFile
14、01 000e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 utf-8 TestClass.java
15、0c 0007 0008 NameAndType #7:#8
16、0c 0005 0006 NameAndType #5:#6
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 utf-8 org/fenixsoft/clazz/TestClass
18、01 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高階特性與最佳實踐這本書,很經典,建議小夥伴們可以看看。
謝謝大家!