歡迎關注微信公衆號: JueCodejava
正若有一句名言:代碼編譯的結果從本地機器碼變爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。 Java語言爲何能write once, run anywhere? 這個實際上是由於和各類不一樣平臺相關的虛擬機,這些虛擬機均可以載入和執行同平臺無關的字節碼。今天咱們就來學習下Class類文件結構的一些知識。編程
Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符。Class文件中只有兩種數據類型:無符號數和表。數組
無符號數屬於基本的數據類型,有u1, u2, u4, u8,分別表明1個字節、2個字節、4個字節和8個字節的無符號數。bash
表則是由多個無符號數或者其餘表複合而成的數據類型。全部表都習慣以_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文件和上面的文件格式來挨個拆解。
首先看到前面三個選項,分別是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版本,一次類推。
緊接着主次版本號以後的是常量池。
常量池能夠理解爲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虛擬機。
緊接着常量池後的是訪問標誌。
在常量池以後緊接這兩個字節是訪問標誌,識別一些類或者接口層次的訪問信息:
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
在訪問標誌後分別是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。
字段表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
複製代碼
緊跟着字段表以後的就是方法表集合。
方法表集合和字段表集合很相似,有一個區別就是用描述符描述方法時,須要先參數列表後返回值,好比
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代碼編譯成的字節碼指令。
最後一個格式就是屬性表集合了。
虛擬機規範預約義的屬性有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
複製代碼
能讀懂Class類文件結構是理解虛擬機的入門功課,本次分享從一個簡單例子詳細闡述了類文件的結構格式,有一些細節沒有仔細說明,好比屬性表的另外的屬性,還有常量池中數據項,屬性表中異常表。可是有了上面的知識儲備,自行分析剩下的就不是什麼問題了。
另外,本文的思路和例子也是參考深刻理解Java虛擬機: JVM高級特性與最佳實踐這本書,很經典,建議小夥伴們能夠看看。
謝謝你們!