歡迎關(guān)注微信公眾號(hào): JueCode
正如有一句名言:代碼編譯的結(jié)果從本地機(jī)器碼變?yōu)樽止?jié)碼,是存儲(chǔ)格式發(fā)展的一小步,卻是編程語言發(fā)展的一大步。 Java語言為什么能write once, run anywhere? 這個(gè)其實(shí)是因?yàn)楹透鞣N不同平臺(tái)相關(guān)的虛擬機(jī),這些虛擬機(jī)都可以載入和執(zhí)行同平臺(tái)無關(guān)的字節(jié)碼。今天我們就來學(xué)習(xí)下Class類文件結(jié)構(gòu)的一些知識(shí)。
1.類文件結(jié)構(gòu)
Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符。Class文件中只有兩種數(shù)據(jù)類型:無符號(hào)數(shù)和表。
無符號(hào)數(shù)屬于基本的數(shù)據(jù)類型,有u1, u2, u4, u8,分別代表1個(gè)字節(jié)、2個(gè)字節(jié)、4個(gè)字節(jié)和8個(gè)字節(jié)的無符號(hào)數(shù)。
表則是由多個(gè)無符號(hào)數(shù)或者其他表復(fù)合而成的數(shù)據(jù)類型。所有表都習(xí)慣以_info結(jié)尾。目前有14個(gè)表格類型:
名稱 | 解釋 |
---|---|
CONSTANT_utf8_info | utf-8編碼的字符串 |
CONSTANT_Integer_info | 整形字面量 |
CONSTANT_Float_info | 浮點(diǎn)型字面量 |
CONSTANT_Long_info | 長(zhǎng)整型字面量 |
CONSTANT_Double_info | 雙精度浮點(diǎn)型字面量 |
CONSTANT_Class_info | 類或接口的符號(hào)引用 |
CONSTANT_String_info | 字符串類型字面量 |
CONSTANT_Fieldref_info | 字段的符號(hào)引用 |
CONSTANT_Methodref_info | 類中方法的符號(hào)引用 |
CONSTANT_Interface_Methodref_info | 接口中方法的符號(hào)引用 |
CONSTANT_NameAndType_info | 字段或方法的部分符號(hào)引用 |
CONSTANT_MethodHandle_info | 表示方法句柄 |
CONSTANT_MethodType_info | 表示方法類型 |
CONSTANT_InvokeDynamic_info | 表示一個(gè)動(dòng)態(tài)方法調(diào)用點(diǎn) |
整個(gè)Class文件是有順序的,整個(gè)格式如下面的表格:
類型 | 名稱 | 數(shù)量 |
---|---|---|
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文件格式都是嚴(yán)格按照上面順序,當(dāng)然有的類型可能沒有,比如一個(gè)類沒有實(shí)現(xiàn)接口,那么interfaces_count 的數(shù)值就為0,后面的interfaces就沒有,以此類推。
下面我們看一個(gè)簡(jiǎn)單的栗子來分析Class文件結(jié)構(gòu)。
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
現(xiàn)在看這個(gè)十六進(jìn)制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個(gè)
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
接下來對(duì)照著這個(gè)十六進(jìn)制class文件和上面的文件格式來挨個(gè)拆解。
2.MagicNumber/version
首先看到前面三個(gè)選項(xiàng),分別是MagicNumber minor_version major_version
其中MagicNumber是固定4個(gè)字節(jié)的常量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的版本,十六進(jìn)制的34轉(zhuǎn)化為十進(jìn)制就是52,也就是對(duì)應(yīng)jdk 1.8版本,50對(duì)應(yīng)的是jdk 1.6版本,一次類推。
緊接著主次版本號(hào)之后的是常量池。
3.常量池
常量池可以理解為Class文件中的資源倉庫,是占用Class文件空間最大的數(shù)據(jù)項(xiàng)目之一。
常量池中常量的數(shù)量是不固定的,所以在常量池入口放置一項(xiàng)u2類型的數(shù)據(jù)代表常量池容易計(jì)數(shù)值,有個(gè)點(diǎn)需要注意這個(gè)容量計(jì)數(shù)是從1而不是0開始。第0項(xiàng)常量空出來是表達(dá)“不引用任何一個(gè)常量池項(xiàng)目”。
看下我們的栗子, 0x0013即十進(jìn)制的19,代表常量池中有18項(xiàng)常量
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 //常量池
常量池中主要存放兩大類常量:字面量和符號(hào)引用。
字面量接近Java中的常量概念,比如字符串,聲明為final的常量值等。
符號(hào)引用包括下面三類:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
常量池中的每一項(xiàng)常量都是一個(gè)表,不同的表是有不同的結(jié)構(gòu),接下來我們來看看14種表的具體含義:
名稱 | 標(biāo)志 | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | utf-8編碼的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮點(diǎn)型字面量 |
CONSTANT_Long_info | 5 | 長(zhǎng)整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點(diǎn)型字面量 |
CONSTANT_Class_info | 7 | 類或接口的符號(hào)引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號(hào)引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號(hào)引用 |
CONSTANT_Interface_Methodref_info | 11 | 接口中方法的符號(hào)引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符號(hào)引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 表示方法類型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個(gè)動(dòng)態(tài)方法調(diào)用點(diǎn) |
通過命令
javap -verbose TestClass
就可以把上面的18個(gè)常量都計(jì)算出來,省得自己挨個(gè)根據(jù)ASCII碼進(jìn)行計(jì)算,得到下面的常量表:
常量池//常量池 18個(gè)
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
舉個(gè)栗子,比如第三個(gè)開頭是07,那么就是對(duì)應(yīng)CONSTANT_Class_info這個(gè)info,而CONSTANT_Class_info對(duì)應(yīng)的是下面的數(shù)據(jù)結(jié)構(gòu):
類型 | 名稱 | 數(shù)量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
類型 | 名稱 | 數(shù)量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
那么緊跟07 后面的11就是索引第11項(xiàng)常量的意思,第11項(xiàng)是01 0003 69 6e 63, 其中tag是01,也就是CONSTANT_utf8_info這個(gè)info,它的數(shù)據(jù)結(jié)構(gòu):
類型 | 名稱 | 數(shù)量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
所以,長(zhǎng)度是3,往后數(shù)三個(gè)字節(jié)就是69 6e 63,對(duì)應(yīng)的就是inc,這個(gè)也就是方法的名稱,其他的都是這樣的分析方式:
首先找到tag對(duì)應(yīng)的表數(shù)據(jù)結(jié)構(gòu),然后根據(jù)數(shù)據(jù)結(jié)構(gòu)拆分。
篇幅所限,其他的常量項(xiàng)的結(jié)構(gòu)可以參考深入理解Java虛擬機(jī)。
緊接著常量池后的是訪問標(biāo)志。
4.訪問標(biāo)志
在常量池之后緊接這兩個(gè)字節(jié)是訪問標(biāo)志,識(shí)別一些類或者接口層次的訪問信息:
Class是類或者接口
是否public
是否abstract
是否final
具體的標(biāo)志位和含義如下面表格:
名稱 | 標(biāo)志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為public |
ACC_FINAL | 0x0010 | 是否為final |
ACC_SUPER | 0x0020 | JDK 1.0.2之后編譯出來的類這個(gè)標(biāo)志都為真 |
ACC_INTERFACE | 0x0200 | 是否為一個(gè)接口 |
ACC_ABSTRACT | 0x0400 | 是否為abstract類型 |
ACC_SYNTHETIC | 0x1000 | 標(biāo)識(shí)這個(gè)類并非由用戶代碼產(chǎn)生 |
ACC_ANNOTATION | 0x2000 | 是否是注解 |
ACC_ENUM | 0x4000 | 是否是枚舉 |
在我們這個(gè)栗子中類是public 是JDK1.8編譯出來的,所以access_flags的值為:ACC_PUBLIC | ACC_SUPER = 0x0021
5.類索引/父類索引/接口索引
在訪問標(biāo)志后分別是this_class/super_class/interfaces_count
0003 //this_class 確定這個(gè)類的全限定名
0004 //super_class java.lang.Object該值就是0000
0000 //interfaces_count 該類沒有實(shí)現(xiàn)任何接口,接口的索引表不占用任何字節(jié)
有的小伙伴就要急了,上面的0003為什么代表this_class?其實(shí)這個(gè)0003就是在常量池中的索引,回顧前面常量池中第3的索引是:07 0011這個(gè)是CONSTANT_Class_info的數(shù)據(jù)結(jié)構(gòu),指向第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
這個(gè)是CONSTANT_utf8_info的數(shù)據(jù)結(jié)構(gòu),對(duì)應(yīng)就是
org/fenixsoft/clazz/TestClass
這個(gè)就是類的全限定名。
其它兩個(gè)的分析以此類推,在這個(gè)例子中沒有實(shí)現(xiàn)接口,所以接口數(shù)量是0,也就沒有后面的interfaces。
緊接著的就是fields_count和fields。
6.字段表集合
字段表field_info用于描述類和接口中聲明的變量。變量包括類級(jí)變量和實(shí)例級(jí)變量,但是不包括方法中的變量。描述字段的信息都有哪些?有作用域(public/private/protect等),static,字段名字,字段數(shù)據(jù)類型,其中可以用布爾類型描述的有:
字段的作用域,public/private/protected
實(shí)例變量還是類變量,static
可變性,final
并發(fā)可見性, volatile
可否被序列化, transient
類似與上面的access_flags, 能用布爾類型表示的定義下面的標(biāo)志位:
名稱 | 標(biāo)志值 |
---|---|
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 |
不能用布爾類型描述的有:
字段名字
字段數(shù)據(jù)類型,基本類型/對(duì)象/數(shù)組
字段名稱肯定是索引常量池中的數(shù)據(jù)項(xiàng),字段數(shù)據(jù)類型呢?專門定義了描述符來標(biāo)識(shí)數(shù)據(jù)類型, 對(duì)象類型用字符L加對(duì)象的全限定名來表示:
標(biāo)識(shí)字符 | 含義 | 標(biāo)識(shí)字符 | 含義 |
---|---|---|---|
B | 基本類型byte | J | 基本類型long |
C | 基本類型char | S | 基本類型short |
D | 基本類型double | Z | 基本類型boolean |
F | 基本類型float | V | 特殊類型void |
I | 基本類型int | L | 對(duì)象類型,如L/java/lang/Object |
對(duì)于數(shù)組類型,每一個(gè)維度使用一個(gè)前置的“[”字符來描述,如“String[][]”表示為“[[Ljava/lang/String;”
字段表也有專門的結(jié)構(gòu), descriptor_index之后可以跟著屬性表集合存儲(chǔ)一些額外的信息,比如private static int m = 123, 那么可能會(huì)有一項(xiàng)ConstantValue的屬性存儲(chǔ)123這個(gè)值。
類型 | 名稱 | 數(shù)量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
對(duì)于我們的例子TestClass, private int m;
//fields_count
0001
//fields
0002 //private
0005 //m
0006 //I
0000 //attribute_count
緊跟著字段表之后的就是方法表集合。
7.方法表集合
方法表集合和字段表集合很類似,有一個(gè)區(qū)別就是用描述符描述方法時(shí),需要先參數(shù)列表后返回值,比如
void inc() ------> ()V
java.lang.String toString(int index) ---> (I)Ljava/lang/String
跟屬性表一樣,方法表也有專門的數(shù)據(jù)結(jié)構(gòu):
類型 | 名稱 | 數(shù)量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
在TestClass中有兩個(gè)方法,一個(gè)是默認(rèn)構(gòu)造函數(shù),一個(gè)是方法inc
//methods_count,編譯器添加的實(shí)例構(gòu)造器<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代碼編譯成的字節(jié)碼指令。
最后一個(gè)格式就是屬性表集合了。
8.屬性表集合
虛擬機(jī)規(guī)范預(yù)定義的屬性有21項(xiàng),這里簡(jiǎn)單看下常用的幾項(xiàng):
屬性 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java代碼編譯成的字節(jié)碼指令 |
ConstantValue | 字段表 | final關(guān)鍵字定義的常量值 |
LineNumberTable | Code屬性 | Java源碼的行號(hào)與字節(jié)碼指令的對(duì)應(yīng)關(guān)系 |
SourceFile | 類文件 | 記錄源文件名稱 |
屬性表結(jié)構(gòu)
類型 | 名稱 | 數(shù)量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
其中Code屬性表的結(jié)構(gòu), attribute_name_index是指向常量池的索引,這里就是'Code'.
類型 | 名稱 | 數(shù)量 |
---|---|---|
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 操作數(shù)棧
0001 //max_locals 局部變量表需要的存儲(chǔ)空間 單位slot
00000005 //code_length 字節(jié)碼長(zhǎng)度
2a b7 00 01 b1 //code 存儲(chǔ)字節(jié)碼指令的一序列字節(jié)流
0000 //exception_table_length
0001 //attributes_count--->Code的屬性
//LineNumberTable描述Java源碼行號(hào)與字節(jié)碼行號(hào)之間的對(duì)應(yīng)關(guān)系
000a //attribute_name_index
00000006 //attribute_length
0001 //line_number_table_length
0000 //start_pc 字節(jié)碼行號(hào)
0003 //Java源碼行號(hào)
//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 存儲(chǔ)字節(jié)碼指令的一序列字節(jié)流
0000
0001
//LineNumberTable
000a
00000006
0001
0000
0006
0001
//SourceFile
000d //SourceFile
00000002
000e
9.總結(jié)
能讀懂Class類文件結(jié)構(gòu)是理解虛擬機(jī)的入門功課,本次分享從一個(gè)簡(jiǎn)單例子詳細(xì)闡述了類文件的結(jié)構(gòu)格式,有一些細(xì)節(jié)沒有仔細(xì)說明,比如屬性表的另外的屬性,還有常量池中數(shù)據(jù)項(xiàng),屬性表中異常表。但是有了上面的知識(shí)儲(chǔ)備,自行分析剩下的就不是什么問題了。
另外,本文的思路和例子也是參考深入理解Java虛擬機(jī): JVM高級(jí)特性與最佳實(shí)踐這本書,很經(jīng)典,建議小伙伴們可以看看。
謝謝大家!