作者 | 東升的思考
責(zé)編 | Elle
不啰嗦,直接從最最簡單的一段JAVA源代碼開啟Java整體字節(jié)碼分析之旅。
Java 源碼文件
package com.dskj.jvm.bytecode;
public class MyTest1 {
private int a = 1;
public intgetA {
return a;
}
public voidsetA(int a) {
this.a = a;
}
}
Java字節(jié)碼文件
IDEA工具編譯代碼后,Terminal 終端控制臺,進(jìn)入到生成class文件的目錄下。
執(zhí)行如下命令:
javap -verbose com.dskj.jvm.bytecode.MyTest1
生成字節(jié)碼文件內(nèi)容:
Classfile
/.../classes/com/dskj/jvm/bytecode/MyTest.class
Last modified Jul 31, 2018; size 489 bytes
MD5 checksum bdb537edd2d216ea99d6ce529073ee42
Compiled from "MyTest1.java"
public class com.dskj.jvm.bytecode.MyTest
minor version: 0
major version: 52 # JDK最大版本號
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: #
#1 = Methodref #4.#20 // java/lang/Object."<init>":V
#2 = Fieldref #3.#21 // com/dskj/jvm/bytecode/MyTest1.a:I
#3 = Class #22 // com/dskj/jvm/bytecode/MyTest1
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/dskj/jvm/bytecode/MyTest1;
#14 = Utf8 getA
#15 = Utf8 I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest1.java
#20 = NameAndType #7:#8 // "<init>":V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/dskj/jvm/bytecode/MyTest1
#23 = Utf8 java/lang/Object
{
public com.dskj.jvm.bytecode.MyTest1;
descriptor: V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 6: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/dskj/jvm/bytecode/MyTest1;
public int getA;
descriptor: I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/dskj/jvm/bytecode/MyTest1;
public void setA(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 15: 0
line 16: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/dskj/jvm/bytecode/MyTest1;
0 6 1 a I
}
SourceFile: "MyTest1.java”
Java字節(jié)碼十六進(jìn)制
mac操作系統(tǒng)下建議使用 Hex Fiend 工具查看 MyTest1.class 文件的十六進(jìn)制格式。
十六進(jìn)制文本如下,便于后續(xù)分析使用:
CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 09 00 03 00 15 07 00 16 07 00 17 01 00 01 61 01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 1F 4C 63 6F 6D 2F 64 73 6B 6A 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 4D 79 54 65 73 74 31 3B 01 00 04 67 65 74 41 01 00 03 28 29 49 01 00 04 73 65 74 41 01 00 04 28 49 29 56 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0C 4D 79 54 65 73 74 31 2E 6A 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00 1D 63 6F 6D 2F 64 73 6B 6A 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 4D 79 54 65 73 74 31 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00 06 00 00 00 03 00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 06 00 04 00 08 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00 00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 0B 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 00 01 00 10 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 0F 00 05 00 10 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 00 06 00 01 00 01 00 12 00 00 00 02 00 13
Java字節(jié)碼結(jié)構(gòu)分析
前面都是鋪墊,來到重磅分析的一節(jié)。
Java字節(jié)碼整體結(jié)構(gòu)如下圖所示,以下圖示以不同緯度展示了字節(jié)碼結(jié)構(gòu)中所包含的關(guān)鍵內(nèi)容。
Java字節(jié)碼整體結(jié)構(gòu)圖:
完整的Java字節(jié)碼結(jié)構(gòu)圖:
接下來結(jié)合十六進(jìn)制格式的 class 文件,參照 Java字節(jié)碼文件來剖析下都包含了哪些內(nèi)容。
1)4個字節(jié),Magic Number
魔數(shù),值為0xCAFEBABE,這是Java創(chuàng)始人James Gosling制定
2)2+2個字節(jié),Version
包括minor_version和major_version,major_version:1.1(45),1.2(46),1.3(47),1.4(48),1.5(49),1.6(50),1.7(51),1.8(52),1.9(53),1.10(54)
3)2+n個字節(jié),Constant Pool
包括字符串常量、數(shù)值常量等
4)2個字節(jié),Access Flags
訪問標(biāo)記,標(biāo)記當(dāng)前的類是public、final、abstract等等,是不是滿足某些特定要求。
5)2個字節(jié),This Class Name
當(dāng)前類的名字
6)2個字節(jié),Super Class Name
當(dāng)前類所屬父類的名字
7)2+n個字節(jié),Interfaces
當(dāng)前類所實現(xiàn)的接口
8)2+n個字節(jié),F(xiàn)ields
字段表,描述了當(dāng)前類的字段的各種各樣的信息
9)2+n個字節(jié),Methods
方法表,當(dāng)前類所定義的方法,這部分內(nèi)容相對比以上字節(jié)結(jié)構(gòu)是比較不容易理解
因為在我們一個類的定義當(dāng)中方法是最常見的,方法里面包含若干的重要信息,包含簽名、訪問修飾符、名字、方法的執(zhí)行代碼邏輯、返回值等等。
這些方法也是以信息的形式存儲在編譯之后的字節(jié)碼class文件當(dāng)中,接下來,JVM去執(zhí)行字節(jié)碼文件時,當(dāng)你調(diào)用某個特定方法時,JVM才能根據(jù)你所編寫的源代碼的意圖去執(zhí)行字節(jié)碼里的指令。
對于這個方法來說,在JVM中最終是形成一條條指令的去執(zhí)行的,也就是說在字節(jié)碼里形成的每一條指令對應(yīng)源碼文件中的每一行源代碼。
這些指令也可以稱作為助記符,比如aload_0,iload_1等。
10)2+n個字節(jié),Attributes
附加屬性
Class字節(jié)碼中有兩種數(shù)據(jù)類型:
-
字節(jié)數(shù)據(jù)直接量:這是基本的數(shù)據(jù)類型。共細(xì)分為u1、u2、u4、u8四種,分別代表連續(xù)的1個字節(jié)、2個字節(jié)、4個字節(jié)、8個字節(jié)組成的整體數(shù)據(jù)。
-
表(數(shù)組),是一種復(fù)合的數(shù)據(jù)結(jié)構(gòu),表是由多個基本數(shù)據(jù)或其他表,按照既定順序組成的大的數(shù)據(jù)集合。表是有結(jié)構(gòu)的,它的結(jié)構(gòu)體現(xiàn)在:組成表的成分所在的位置和順序都是已經(jīng)嚴(yán)格定義好的。
接下來,我們使用 javap -verbose 命令分析一個字節(jié)碼文件時,將會分析該字節(jié)碼文件的魔數(shù)、版本號、常量池、訪問標(biāo)記、類信息、類變量、類的成員變量、類的構(gòu)造方法與類中的方法信息等信息。
4.1 魔數(shù)
魔數(shù):所有的.class字節(jié)碼文件的前4個字節(jié)都是魔數(shù),文件中魔數(shù)為:CA FE BA BE,魔數(shù)值為固定值:0xCAFEBABE(咖啡寶貝?),這個值的獲得很有“浪漫氣息”,其作用是確定這個文件是否為一個能被虛擬機接受的Class文件。
4.2 版本號
版本號:魔數(shù)之后的4個字節(jié)為Class文件版本信息,前兩個字節(jié)表示minor version(次版本號),后兩個字節(jié)表示major version(主版本號)。
這里的版本號為00 00 00 34,換算成十進(jìn)制(3 * 16的1次方 + 4 = 52),表示次版本號為0,主版本號為52。
所以,該文件的版本號為:1.8.0。可以通過java -version命令來驗證這一點。Java的版本號是從45開始的,JDK1.0之后大的主版本號線上加1,如JDK1.1(45)、JDK1.2(46)以此類推JDK1.8(52)。
4.3 常量池
常量池(constant pool):緊接著主版本號之后的就是常量池入口。
一個Java類中定義的很多信息都是由常量池來維護(hù)和描述的,可以將常量池看作是Class文件的資源倉庫,比如說Java類中定義的方法與變量信息,都是存儲在常量池中。由于常量池中常量的數(shù)量是不固定的,故在常量池入口需要放置一項u2類型的數(shù)據(jù),代表常量池容量計數(shù)值(constant_pool_count)。
這里的容量計數(shù)是從1開始的,十六進(jìn)制數(shù)為:00 18,轉(zhuǎn)換為十進(jìn)制為24,代表常量池中有24項常量,索引值范圍1~24。
常量池數(shù)組中元素的個數(shù) = 常量池數(shù) - 1(其中0暫時不使用),所以Java字節(jié)碼文件中constant_pool中只看到了23項目常量。那為什么容量計數(shù)不從0開始呢?具體原因下一節(jié)說明。
常量池中主要存儲兩類常量:
-
字面量:字面量如文本字符串,Java中聲明為final的常量值等。
-
符號引用:類和接口的全局限定名,字段的名稱和描述符,方法的名稱和描述符等。
4.3.1 常量池總體結(jié)構(gòu)
Java類所對應(yīng)的常量池主要由常量池數(shù)量與常量池數(shù)組(常量表)這兩部分共同構(gòu)成。
常量池數(shù)量緊跟在主版本號后面,占據(jù)2個字節(jié);常量池數(shù)組緊跟在常量池數(shù)量之后。常量池數(shù)組與一般的數(shù)組不同的是,常量池數(shù)組中元素的類型、結(jié)構(gòu)都是不同的,長度當(dāng)然也就不同;但是,每一種元素第一個數(shù)據(jù)都是一個u1類型,該字節(jié)是個標(biāo)志位,占據(jù)1個字節(jié)。
JVM在解析常量池時,會根據(jù)這個u1類型來獲取元素的具體類型。值得注意的是,常量池數(shù)組中元素的個數(shù) = 常量池數(shù) - 1(其中0暫時不使用),目的是滿足某些常量池索引值的數(shù)據(jù)在特定情況下需要表達(dá)「不引用任何一個常量池」的含義;根本原因在于,索引為0也是一個常量(保留常量),只不過它不位于常量表中,這個常量就對應(yīng)值;所以,常量池的索引從1而非0開始。
4.3.2 常量池項目類型
Class文件結(jié)構(gòu)中常量池中實際是有14種數(shù)據(jù)類型的,12~14種數(shù)據(jù)類型是在JDK1.7之后添加進(jìn)來的(新增三種類型分別為:CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info),主要是為了更好的支持動態(tài)語言調(diào)用的。但是,最常用如下所列的列出了11種常規(guī)的數(shù)據(jù)類型:
上述常量都是以「CONSTANT」開頭,以「info」結(jié)尾的常量名。每一個常量包含的信息的段都是不同的,我們可以根據(jù)每一個段自身的起始和結(jié)束位置是什么來進(jìn)行分析。
抽出兩個代表性的常量進(jìn)行解析:
CONSTANT_Utf8_info
如果這個tag的值為1,占1個字節(jié),它就表示的UTF-8編碼的字符串;length,占2個字節(jié),比如length值是4,表示的是從length的下后面讀取4個字節(jié)長度的字符串。
這個就表示CONSTANT_Utf8_info的具體的文本內(nèi)容。就是說根據(jù)length就能夠知道接下來我要讀取多少個字節(jié)才能讀完,這些字節(jié)是由bytes來表示的。
CONSTANT_Fieldref_info
tag是U1類型,值為9。有兩個index值,都是U2類型的,第一個index代表的是指向聲明字段的類或接口描述符CONSTANT_Class_info的索引項,第二個index代表的指向字段描述符CONSTANT_NameAndType_info的索引項。
具體可以理解為當(dāng)我們定義一個字段時,一定是附屬在某一個類上的,所以要先索引到類信息上,可以具體看下CONSTANT_Class_info,其tag是U1類型,值為7,它的index代表指向全限定名常量項的索引,很好理解了。
然后再找到這個字段的描述符,這里指向了會索引到CONSTANT_NameAndType_info,其tag是U1類型,值為12,根據(jù)兩個index的描述可以理解為要有字段或方法的名稱以及字段或方法的描述符即可找到源碼中對應(yīng)的字段和方法。
4.3.3 常量池結(jié)構(gòu)分析
接下來,我們以上述Java字節(jié)碼結(jié)構(gòu)總表為依據(jù)分析下Java字節(jié)碼十六進(jìn)制對應(yīng)到Java字節(jié)碼文件中的constant_pool常量池。
Java字節(jié)碼十六進(jìn)制:
從第9位開始的十六進(jìn)制
0A 00 04 00 14 0A表示值為10,從字節(jié)碼結(jié)構(gòu)總表中找到值為10的是CONSTANT_Methodref_info,有兩個index值,第一個index占用的字節(jié) 00 04 轉(zhuǎn)換為十進(jìn)制為4,第二個index占用的字節(jié)00 14 轉(zhuǎn)化為十進(jìn)制為20。
從Java字節(jié)碼文件中Constant pool定義可看到:
Constant pool: #
#1 = Methodref #4.#20 // java/lang/Object."<init>":V
索引到位置#4和#20,從常量池中找到這兩個索引項如下:
#4 = Class #23 // java/lang/Object
#20 = NameAndType #7:#8 // "<init>":V
這兩個索引正好可以跟結(jié)構(gòu)總表中對應(yīng)上。其中,#4表示的類全限定名為java/lang/Object,而索引20位置又引用了#7:#8。繼續(xù)找到#7和#8:
#7 = Utf8 <init>
#8 = Utf8 V
從第16位開始的十六進(jìn)制
09 00 03 00 15 這個標(biāo)志位值為09,從字節(jié)碼結(jié)構(gòu)總表中找到值為9的常量為CONSTANT_Fieldref_info,其后面跟著兩個index,對應(yīng)十六進(jìn)制轉(zhuǎn)換為十進(jìn)制為3和21。
#2 = Fieldref #3.#21 // com/dskj/jvm/bytecode/MyTest1.a:I
對應(yīng)有兩個索引項#3和#21,如下所示:
#3 = Class #22 // com/dskj/jvm/bytecode/MyTest1
#21 = NameAndType #5:#6 // a:I
索引項#3引用了索引項#22,索引項#21引用了索引項#5:#6
#22 = Utf8 com/dskj/jvm/bytecode/MyTest1
#5 = Utf8 a
#6 = Utf8 I
根據(jù)以上,#5表示的變量名為a,#6表示的變量a的返回類型是I,即int類型的。也就知道了#2 = Fileldref,對應(yīng)的是com/dskj/jvm/bytecode/MyTest1.a:I。
對應(yīng)到MyTest1類的變量:
private int a = 1;
從第21位開始的十六進(jìn)制
07 00 16 標(biāo)志位為07,值為7字節(jié)碼結(jié)構(gòu)總表中對應(yīng)常量CONSTANT_Class_info,索引占用2個字節(jié),對應(yīng)轉(zhuǎn)換為十進(jìn)制為22。
#3 = Class #22 // com/dskj/jvm/bytecode/MyTest1
#22 = Utf8 com/dskj/jvm/bytecode/MyTest1
從第27位開始的十六進(jìn)制
十六進(jìn)制字節(jié)碼文件:
01 00 01 61 01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
查找標(biāo)志位為01 ,值為1的結(jié)構(gòu)總表常量為CONSTANT_Utf8-info,length的占用2個字節(jié)十六進(jìn)制為 00 01 ,那么length長度就是1(轉(zhuǎn)換為十進(jìn)制的值,即0 * 16的一次方 + 1),后面找到1個字節(jié)為61,通過HexFiend工具也能看到指向了a。
所以,找到的十六進(jìn)制:01 00 01 61
常量池中進(jìn)一步印證下:
#6 = Utf8 I
十六進(jìn)制字節(jié)碼文件:
01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65...
繼續(xù)查找標(biāo)志位為01 ,值為1的結(jié)構(gòu)總表常量為CONSTANT_Utf8-info,length的占用2個字節(jié)十六進(jìn)制為 00 01 ,那么length長度就是1,后面找到1個字節(jié)為49,通過HexFiend工具也能看到指向了I。
所以,找到的十六進(jìn)制:01 00 01 49
常量池中進(jìn)一步印證下:
#6 = Utf8 I
十六進(jìn)制字節(jié)碼文件:
01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65...
繼續(xù)查找標(biāo)志位為01 ,值為1的結(jié)構(gòu)總表常量為CONSTANT_Utf8-info,length的占用2個字節(jié)十六進(jìn)制為 00 06 ,那么length長度就是6(轉(zhuǎn)換為十進(jìn)制的值,即0 * 16的一次方 + 6),后面找到6個字節(jié)為 3C 69 6E 69 74 3E,通過HexFiend工具也能看到指向了<init>。
所以,找到的十六進(jìn)制:01 00 06 3C 69 6E 69 74 3E
常量池中進(jìn)一步印證下:
#7 = Utf8 <init>
以此類推,最終都能通過十六進(jìn)制字節(jié)碼并結(jié)合字節(jié)碼結(jié)構(gòu)總表分析在常量池中找到對應(yīng)的字節(jié)碼內(nèi)容。
4.4 訪問標(biāo)志
訪問標(biāo)志信息包括該Class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,是否被聲明成final。通過上面的MyTest1源代碼,我們知道該文件是類并且是public的。
Access_Flag訪問標(biāo)志結(jié)構(gòu)表:
上述MyTest1類十六進(jìn)制字節(jié)碼中的位置:0x 00 21
這個 0x 00 21 是訪問標(biāo)志結(jié)構(gòu)表中的 0x 00 20 和 0x 00 01 的并集,表示 ACC_PUBLIC 與 ACC_SUPER。
public class com.dskj.jvm.bytecode.MyTest1
...
flags: ACC_PUBLIC, ACC_SUPER
訪問標(biāo)志之后的是This Class Name,對應(yīng)十六進(jìn)制為 0x 00 03
在常量池項目類型中查找:
#3 = Class #22 // com/dskj/jvm/bytecode/MyTest1
This Class Name之后的是Super Class Name,對應(yīng)十六進(jìn)制為 0x 00 04
在常量池項目類型中查找:
#4 = Class #23 // java/lang/Object
Interfaces
接口包括兩部分,第一個是interfaces_count(接口個數(shù)),第二部分interfaces(接口名)。
當(dāng)前這個類對應(yīng)的十六進(jìn)制:00 00 轉(zhuǎn)換為十進(jìn)制仍然是0,說明當(dāng)前這個類是沒有實現(xiàn)任何接口的。
因此,這個interfaces接口表就不會再出現(xiàn)了。如果接口數(shù)量interfaces_count大于等于1的話,那么這個interfaces接口表是存在的。
4.5 字段表
Fields
字段包括兩部分,第一個是fields_count(字段個數(shù)),第二部分fields(字段名)。
當(dāng)前這個類對應(yīng)的十六進(jìn)制:00 01 轉(zhuǎn)換為十進(jìn)制值為1,說明這個類內(nèi)部有一個字段。
字段表集合
字段表用于描述類和接口中聲明的變量。這里的字段包含了類級別變量以及實例變量,但是不包括方法內(nèi)部聲明的局部變量。
字段表結(jié)構(gòu):
第一個是access_flags訪問標(biāo)志符,如public、private、protected、final、abstract等等。
第二個name_index和第三個descriptor_index兩個構(gòu)成一個字段結(jié)構(gòu)的完整信息。
attributes_count是字段的獨有的信息,如果值是0,后面的attributes也就不存在了。
具體結(jié)構(gòu)示例:
當(dāng)前類字段對應(yīng)的十六進(jìn)制如下所示:
field_info {
u2 access_flags; 0002
u2 name_index; 0005
u2 descriptor_index; 0006
u2 attributes_counts; 0000
attribute_info attributes[attributes_count];
}
0x0002在訪問標(biāo)志結(jié)構(gòu)表中對應(yīng)的是ACC_PRIVATE。
名稱索引 0x0005 與 描述符索引 0x0006 轉(zhuǎn)換為十六進(jìn)制為 5 和 6,從 常量池結(jié)構(gòu)表中查找結(jié)果:
#5 = Utf8 a
#6 = Utf8 I
附加屬性的數(shù)量為0x0000,轉(zhuǎn)換為十進(jìn)制為0,后面的附加屬性attributes也就不會出現(xiàn)了。
4.6 方法表
00 03 // methods_count
00 01 // access_flags
00 07 // name_index
00 08 // descriptor_index
00 01 // attributes_count
00 09 // attribute_name_index
00 00 00 38 // attribute_length
00 02 // 附加屬性的 max_stacks
00 01 // 附加屬性的 max_locals
00 00 00 0A // 附加屬性的 code_length
2A B7 00 01 2A 04 B5 00 02 B1 // code_lengthc長度的字節(jié),具體執(zhí)行的字節(jié)碼指令
00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 06 00 04 00 08 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00 00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 0B 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 00 01 00 10 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 0F 00 05 00 10 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 00 06 00 01 00 01 00 12 00 00 00 02 00 13
Methods
方法包括兩部分,第一個是methods_count(方法個數(shù)),第二部分methods(方法名)。
當(dāng)前這個類對應(yīng)的十六進(jìn)制:00 03轉(zhuǎn)換為十進(jìn)制值為3,說明這個類內(nèi)部有三個方法。
三個方法為:
setA、getA,以及默認(rèn)無參的構(gòu)造方法。
方法表結(jié)構(gòu):
具體含義類似于上述的字段表結(jié)構(gòu)。
access_flags 對應(yīng)的十六進(jìn)制:00 01 在標(biāo)志結(jié)構(gòu)表中查找為ACC_PUBLIC。
name_index名稱索引對應(yīng)十六進(jìn)制 00 07 descriptor_index描述符索引對應(yīng)十六進(jìn)制 00 08
分別轉(zhuǎn)換為十進(jìn)制為 7 和 8,在常量池中查找結(jié)果:
#7 = Utf8 <init> // 表示這個類的構(gòu)造方法
#8 = Utf8 V // 表示不接收任何參數(shù)的不返回結(jié)果的描述符
attributes_count對應(yīng)十六進(jìn)制:00 01 ,其個數(shù)為1,表示會有一個附加屬性。也說明了有一個attributes。
方法的屬性結(jié)構(gòu)構(gòu)成:
方法中的每一個屬性都是一個atrribute_info結(jié)構(gòu)。
atrribute_info {
u2 atrribute_name_index;
u4 attribute_length;
u1 info[atrribute_length];
}
attribute_name_index對應(yīng)十六進(jìn)制為 00 09,在常量池結(jié)構(gòu)表中查找結(jié)果:
#9 = Utf8 Code
從字節(jié)碼中每一個方法中都能體現(xiàn)出來,比如默認(rèn)構(gòu)造方法:
public com.dskj.jvm.bytecode.MyTest1;
descriptor: V
flags: ACC_PUBLIC
Code:
...
然后根據(jù) atrribute_length 對應(yīng)十六進(jìn)制為 00 00 00 38 轉(zhuǎn)換為十進(jìn)制為3 * 16的一次方 + 8 = 56
說明在這個十六進(jìn)制后面找到56個字節(jié)作為Code這個屬性的具體的值。
方法表結(jié)構(gòu):
前三個字段和field_info一樣。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count]
}
方法的屬性結(jié)構(gòu):
-
JVM預(yù)定了部分atrribute,但是編譯器自己也可以實現(xiàn)自己的atrribute寫入class文件里,供運行時使用。
-
不同的attribute通過attribute_name_index來區(qū)分。
Code結(jié)構(gòu):
Code attribute的作用是保存該方法的結(jié)構(gòu),如所對應(yīng)的字節(jié)碼。
-
attribute_length表示attribute所包含的字節(jié)數(shù),不包含atrribute_name_index和attribute_length字段。
-
max_stack表示這個方法運行的任何時刻所能達(dá)到的操作數(shù)棧的最大深度。// 00 02
-
max_locals表示方法執(zhí)行期間創(chuàng)建的局部變量的數(shù)目,包含用來表示傳入的參數(shù)的局部變量的。// 00 01
-
code_length表示該方法所包含的字節(jié)碼的字節(jié)數(shù)以及具體的指令碼。也即助記符。// 00 00 00 0A 轉(zhuǎn)換為十進(jìn)制值為10,即跟著后面的10個字節(jié) 2A B7 00 01 2A 04 B5 00 02 B1 這些是字節(jié)碼具體指令,對應(yīng)到構(gòu)造方法的字節(jié)碼:
那么,這些十六進(jìn)制是怎么和下面的助記符對應(yīng)的呢?
我們通過jclasslib工具(字節(jié)碼查看工具,支持IDEA插件形式安裝)查看時,點擊助記符的鏈接會跳到Oracle官網(wǎng)可查看具體詳細(xì)解釋。第一個助記符: 0: aload_0 打開鏈接可以看到:
鏈接地址:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.aload_n
具體解釋內(nèi)容所示:
aload_<n>
Operation
Load reference from local variable
Format
aload_<n>
Forms
aload_0 = 42 (0x2a) // 通過這里就能直接看到 aload_0 對應(yīng)的十進(jìn)制是42,轉(zhuǎn)換為十六進(jìn)制就是 0x2a,對應(yīng)字節(jié)碼文件中的 2A
aload_1 = 43 (0x2b)
aload_2 = 44 (0x2c)
aload_3 = 45 (0x2d)
Description
The <n> must be an index into the local variable array of the current frame (§2.6). The local variable at <n> must contain a reference. The objectref in the local variable at <n> is pushed onto the operand stack.
這個<n>必須是一個到當(dāng)前棧幀局部變量數(shù)組的一個索引,位于<n>位置上的局部變量會包含一個引用,位于<n>位置上的局部變量的這個引用會被推送到棧頂(準(zhǔn)備進(jìn)行操作)。
第二個助記符:
1: invokespecial #1 // Method java/lang/Object."<init>":V
連接地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokespecial
invokespecial
Operation
Invoke instance method; special handling for superclass, private, and instance initialization method invocations
Format
invokespecial
indexbyte1
indexbyte2
Forms
invokespecial = 183 (0xb7)
Operand Stack
..., objectref, [arg1, [arg2 ...]] →
...
-
具體字節(jié)碼即是該方法被調(diào)用時,虛擬機所執(zhí)行的字節(jié)碼。
-
exception_table,這里存放的是處理異常的信息。
-
每個exception_table表項由start_pc,end_pc,handler_pc,catch_type組成。
-
start_pc和end_pc表示在code數(shù)組中的從start_pc到end_pc處(包含start_pc,不包含end_pc)的指令拋出的異常會由這個表項來處理。
-
handler_pc表示處理異常的代碼的開始處。catch_type表示會被處理的異常類型,它指向常量池里的一個異常類。當(dāng)catch_type為0時,表示處理所有的異常。
附加屬性
LineNumberTable:這個屬性用來表示code數(shù)組中的字節(jié)碼和Java代碼行數(shù)之間的關(guān)系。這個屬性可以用來在調(diào)試的時候定位代碼的執(zhí)行行數(shù)。
LocalVariableTable:局部變量表,當(dāng)前類中只有唯一的局部變量,而這個局部變量就是this當(dāng)前對象。
局部變量表屬性類似于行號表屬性。
請注意:
Java源代碼角度:Java類中的實例方法中可以直接使用this。
Java字節(jié)碼角度: Java類中的非靜態(tài)方法,即實例方法中的這個this實際是通過編譯器隱示的作為方法的第一個參數(shù)傳遞進(jìn)來(有點類似于Python中的方法,其方法中的第一個參數(shù)都會傳遞一個self變量,表示當(dāng)前對象本身)。這樣使得每一個實例方法內(nèi)部都可以很順利的訪問this。換句話說針對類的實例方法它至少會有一個LocalVariable局部變量,這個變量就是this。
4.7 字段/方法描述符
在JVM規(guī)范中,每個變量/字段都有描述信息,描述信息主要的作用是描述字段的數(shù)據(jù)類型、方法的參數(shù)列表(包括數(shù)量、類型與順序)與返回值。根據(jù)描述符規(guī)則,基本數(shù)據(jù)類型和代表無返回值的void類型都用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示。為了壓縮字節(jié)碼文件的體積,對于基本數(shù)據(jù)類型,JVM都只使用一個大寫字母來表示,如下所示:
B - byte,C - char,D - double,F(xiàn) - float,I - int,J - long,S - short,Z - boolean,V - void,L - 對象類型,如Ljava/lang/String;
數(shù)組類型: 針對數(shù)組類型來說,每一個維度使用一個前置的[來表示,如:
int數(shù)組被記錄[I,String[]二維數(shù)組被記錄為[[Ljava/lang/String;
方法描述符
用描述符描述方法時,按照先參數(shù)列表,后返回值的順序來描述。參數(shù)列表按照參數(shù)的嚴(yán)格順序放在一組之內(nèi),如方法:
String getInfoByIdAndName(int id, String name),該方法的描述符為:(I, Ljava/lang/String;)Ljava/lang/String;
Java字節(jié)碼文件的工具推薦:
https://github.com/ingokegel/jclasslib






