作者 | 東升的思考
責編 | Elle
不啰嗦,直接從最最簡單的一段JAVA源代碼開啟Java整體字節碼分析之旅。
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字節碼文件
IDEA工具編譯代碼后,Terminal 終端控制臺,進入到生成class文件的目錄下。
執行如下命令:
javap -verbose com.dskj.jvm.bytecode.MyTest1
生成字節碼文件內容:
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字節碼十六進制
mac操作系統下建議使用 Hex Fiend 工具查看 MyTest1.class 文件的十六進制格式。
十六進制文本如下,便于后續分析使用:
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字節碼結構分析
前面都是鋪墊,來到重磅分析的一節。
Java字節碼整體結構如下圖所示,以下圖示以不同緯度展示了字節碼結構中所包含的關鍵內容。
Java字節碼整體結構圖:
完整的Java字節碼結構圖:
接下來結合十六進制格式的 class 文件,參照 Java字節碼文件來剖析下都包含了哪些內容。
1)4個字節,Magic Number
魔數,值為0xCAFEBABE,這是Java創始人James Gosling制定
2)2+2個字節,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個字節,Constant Pool
包括字符串常量、數值常量等
4)2個字節,Access Flags
訪問標記,標記當前的類是public、final、abstract等等,是不是滿足某些特定要求。
5)2個字節,This Class Name
當前類的名字
6)2個字節,Super Class Name
當前類所屬父類的名字
7)2+n個字節,Interfaces
當前類所實現的接口
8)2+n個字節,Fields
字段表,描述了當前類的字段的各種各樣的信息
9)2+n個字節,Methods
方法表,當前類所定義的方法,這部分內容相對比以上字節結構是比較不容易理解
因為在我們一個類的定義當中方法是最常見的,方法里面包含若干的重要信息,包含簽名、訪問修飾符、名字、方法的執行代碼邏輯、返回值等等。
這些方法也是以信息的形式存儲在編譯之后的字節碼class文件當中,接下來,JVM去執行字節碼文件時,當你調用某個特定方法時,JVM才能根據你所編寫的源代碼的意圖去執行字節碼里的指令。
對于這個方法來說,在JVM中最終是形成一條條指令的去執行的,也就是說在字節碼里形成的每一條指令對應源碼文件中的每一行源代碼。
這些指令也可以稱作為助記符,比如aload_0,iload_1等。
10)2+n個字節,Attributes
附加屬性
Class字節碼中有兩種數據類型:
-
字節數據直接量:這是基本的數據類型。共細分為u1、u2、u4、u8四種,分別代表連續的1個字節、2個字節、4個字節、8個字節組成的整體數據。
-
表(數組),是一種復合的數據結構,表是由多個基本數據或其他表,按照既定順序組成的大的數據集合。表是有結構的,它的結構體現在:組成表的成分所在的位置和順序都是已經嚴格定義好的。
接下來,我們使用 javap -verbose 命令分析一個字節碼文件時,將會分析該字節碼文件的魔數、版本號、常量池、訪問標記、類信息、類變量、類的成員變量、類的構造方法與類中的方法信息等信息。
4.1 魔數
魔數:所有的.class字節碼文件的前4個字節都是魔數,文件中魔數為:CA FE BA BE,魔數值為固定值:0xCAFEBABE(咖啡寶貝?),這個值的獲得很有“浪漫氣息”,其作用是確定這個文件是否為一個能被虛擬機接受的Class文件。
4.2 版本號
版本號:魔數之后的4個字節為Class文件版本信息,前兩個字節表示minor version(次版本號),后兩個字節表示major version(主版本號)。
這里的版本號為00 00 00 34,換算成十進制(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類中定義的很多信息都是由常量池來維護和描述的,可以將常量池看作是Class文件的資源倉庫,比如說Java類中定義的方法與變量信息,都是存儲在常量池中。由于常量池中常量的數量是不固定的,故在常量池入口需要放置一項u2類型的數據,代表常量池容量計數值(constant_pool_count)。
這里的容量計數是從1開始的,十六進制數為:00 18,轉換為十進制為24,代表常量池中有24項常量,索引值范圍1~24。
常量池數組中元素的個數 = 常量池數 - 1(其中0暫時不使用),所以Java字節碼文件中constant_pool中只看到了23項目常量。那為什么容量計數不從0開始呢?具體原因下一節說明。
常量池中主要存儲兩類常量:
-
字面量:字面量如文本字符串,Java中聲明為final的常量值等。
-
符號引用:類和接口的全局限定名,字段的名稱和描述符,方法的名稱和描述符等。
4.3.1 常量池總體結構
Java類所對應的常量池主要由常量池數量與常量池數組(常量表)這兩部分共同構成。
常量池數量緊跟在主版本號后面,占據2個字節;常量池數組緊跟在常量池數量之后。常量池數組與一般的數組不同的是,常量池數組中元素的類型、結構都是不同的,長度當然也就不同;但是,每一種元素第一個數據都是一個u1類型,該字節是個標志位,占據1個字節。
JVM在解析常量池時,會根據這個u1類型來獲取元素的具體類型。值得注意的是,常量池數組中元素的個數 = 常量池數 - 1(其中0暫時不使用),目的是滿足某些常量池索引值的數據在特定情況下需要表達「不引用任何一個常量池」的含義;根本原因在于,索引為0也是一個常量(保留常量),只不過它不位于常量表中,這個常量就對應值;所以,常量池的索引從1而非0開始。
4.3.2 常量池項目類型
Class文件結構中常量池中實際是有14種數據類型的,12~14種數據類型是在JDK1.7之后添加進來的(新增三種類型分別為:CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info),主要是為了更好的支持動態語言調用的。但是,最常用如下所列的列出了11種常規的數據類型:
上述常量都是以「CONSTANT」開頭,以「info」結尾的常量名。每一個常量包含的信息的段都是不同的,我們可以根據每一個段自身的起始和結束位置是什么來進行分析。
抽出兩個代表性的常量進行解析:
CONSTANT_Utf8_info
如果這個tag的值為1,占1個字節,它就表示的UTF-8編碼的字符串;length,占2個字節,比如length值是4,表示的是從length的下后面讀取4個字節長度的字符串。
這個就表示CONSTANT_Utf8_info的具體的文本內容。就是說根據length就能夠知道接下來我要讀取多少個字節才能讀完,這些字節是由bytes來表示的。
CONSTANT_Fieldref_info
tag是U1類型,值為9。有兩個index值,都是U2類型的,第一個index代表的是指向聲明字段的類或接口描述符CONSTANT_Class_info的索引項,第二個index代表的指向字段描述符CONSTANT_NameAndType_info的索引項。
具體可以理解為當我們定義一個字段時,一定是附屬在某一個類上的,所以要先索引到類信息上,可以具體看下CONSTANT_Class_info,其tag是U1類型,值為7,它的index代表指向全限定名常量項的索引,很好理解了。
然后再找到這個字段的描述符,這里指向了會索引到CONSTANT_NameAndType_info,其tag是U1類型,值為12,根據兩個index的描述可以理解為要有字段或方法的名稱以及字段或方法的描述符即可找到源碼中對應的字段和方法。
4.3.3 常量池結構分析
接下來,我們以上述Java字節碼結構總表為依據分析下Java字節碼十六進制對應到Java字節碼文件中的constant_pool常量池。
Java字節碼十六進制:
從第9位開始的十六進制
0A 00 04 00 14 0A表示值為10,從字節碼結構總表中找到值為10的是CONSTANT_Methodref_info,有兩個index值,第一個index占用的字節 00 04 轉換為十進制為4,第二個index占用的字節00 14 轉化為十進制為20。
從Java字節碼文件中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
這兩個索引正好可以跟結構總表中對應上。其中,#4表示的類全限定名為java/lang/Object,而索引20位置又引用了#7:#8。繼續找到#7和#8:
#7 = Utf8 <init>
#8 = Utf8 V
從第16位開始的十六進制
09 00 03 00 15 這個標志位值為09,從字節碼結構總表中找到值為9的常量為CONSTANT_Fieldref_info,其后面跟著兩個index,對應十六進制轉換為十進制為3和21。
#2 = Fieldref #3.#21 // com/dskj/jvm/bytecode/MyTest1.a:I
對應有兩個索引項#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
根據以上,#5表示的變量名為a,#6表示的變量a的返回類型是I,即int類型的。也就知道了#2 = Fileldref,對應的是com/dskj/jvm/bytecode/MyTest1.a:I。
對應到MyTest1類的變量:
private int a = 1;
從第21位開始的十六進制
07 00 16 標志位為07,值為7字節碼結構總表中對應常量CONSTANT_Class_info,索引占用2個字節,對應轉換為十進制為22。
#3 = Class #22 // com/dskj/jvm/bytecode/MyTest1
#22 = Utf8 com/dskj/jvm/bytecode/MyTest1
從第27位開始的十六進制
十六進制字節碼文件:
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 ,值為1的結構總表常量為CONSTANT_Utf8-info,length的占用2個字節十六進制為 00 01 ,那么length長度就是1(轉換為十進制的值,即0 * 16的一次方 + 1),后面找到1個字節為61,通過HexFiend工具也能看到指向了a。
所以,找到的十六進制:01 00 01 61
常量池中進一步印證下:
#6 = Utf8 I
十六進制字節碼文件:
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 ,值為1的結構總表常量為CONSTANT_Utf8-info,length的占用2個字節十六進制為 00 01 ,那么length長度就是1,后面找到1個字節為49,通過HexFiend工具也能看到指向了I。
所以,找到的十六進制:01 00 01 49
常量池中進一步印證下:
#6 = Utf8 I
十六進制字節碼文件:
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 ,值為1的結構總表常量為CONSTANT_Utf8-info,length的占用2個字節十六進制為 00 06 ,那么length長度就是6(轉換為十進制的值,即0 * 16的一次方 + 6),后面找到6個字節為 3C 69 6E 69 74 3E,通過HexFiend工具也能看到指向了<init>。
所以,找到的十六進制:01 00 06 3C 69 6E 69 74 3E
常量池中進一步印證下:
#7 = Utf8 <init>
以此類推,最終都能通過十六進制字節碼并結合字節碼結構總表分析在常量池中找到對應的字節碼內容。
4.4 訪問標志
訪問標志信息包括該Class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,是否被聲明成final。通過上面的MyTest1源代碼,我們知道該文件是類并且是public的。
Access_Flag訪問標志結構表:
上述MyTest1類十六進制字節碼中的位置:0x 00 21
這個 0x 00 21 是訪問標志結構表中的 0x 00 20 和 0x 00 01 的并集,表示 ACC_PUBLIC 與 ACC_SUPER。
public class com.dskj.jvm.bytecode.MyTest1
...
flags: ACC_PUBLIC, ACC_SUPER
訪問標志之后的是This Class Name,對應十六進制為 0x 00 03
在常量池項目類型中查找:
#3 = Class #22 // com/dskj/jvm/bytecode/MyTest1
This Class Name之后的是Super Class Name,對應十六進制為 0x 00 04
在常量池項目類型中查找:
#4 = Class #23 // java/lang/Object
Interfaces
接口包括兩部分,第一個是interfaces_count(接口個數),第二部分interfaces(接口名)。
當前這個類對應的十六進制:00 00 轉換為十進制仍然是0,說明當前這個類是沒有實現任何接口的。
因此,這個interfaces接口表就不會再出現了。如果接口數量interfaces_count大于等于1的話,那么這個interfaces接口表是存在的。
4.5 字段表
Fields
字段包括兩部分,第一個是fields_count(字段個數),第二部分fields(字段名)。
當前這個類對應的十六進制:00 01 轉換為十進制值為1,說明這個類內部有一個字段。
字段表集合
字段表用于描述類和接口中聲明的變量。這里的字段包含了類級別變量以及實例變量,但是不包括方法內部聲明的局部變量。
字段表結構:
第一個是access_flags訪問標志符,如public、private、protected、final、abstract等等。
第二個name_index和第三個descriptor_index兩個構成一個字段結構的完整信息。
attributes_count是字段的獨有的信息,如果值是0,后面的attributes也就不存在了。
具體結構示例:
當前類字段對應的十六進制如下所示:
field_info {
u2 access_flags; 0002
u2 name_index; 0005
u2 descriptor_index; 0006
u2 attributes_counts; 0000
attribute_info attributes[attributes_count];
}
0x0002在訪問標志結構表中對應的是ACC_PRIVATE。
名稱索引 0x0005 與 描述符索引 0x0006 轉換為十六進制為 5 和 6,從 常量池結構表中查找結果:
#5 = Utf8 a
#6 = Utf8 I
附加屬性的數量為0x0000,轉換為十進制為0,后面的附加屬性attributes也就不會出現了。
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長度的字節,具體執行的字節碼指令
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(方法個數),第二部分methods(方法名)。
當前這個類對應的十六進制:00 03轉換為十進制值為3,說明這個類內部有三個方法。
三個方法為:
setA、getA,以及默認無參的構造方法。
方法表結構:
具體含義類似于上述的字段表結構。
access_flags 對應的十六進制:00 01 在標志結構表中查找為ACC_PUBLIC。
name_index名稱索引對應十六進制 00 07 descriptor_index描述符索引對應十六進制 00 08
分別轉換為十進制為 7 和 8,在常量池中查找結果:
#7 = Utf8 <init> // 表示這個類的構造方法
#8 = Utf8 V // 表示不接收任何參數的不返回結果的描述符
attributes_count對應十六進制:00 01 ,其個數為1,表示會有一個附加屬性。也說明了有一個attributes。
方法的屬性結構構成:
方法中的每一個屬性都是一個atrribute_info結構。
atrribute_info {
u2 atrribute_name_index;
u4 attribute_length;
u1 info[atrribute_length];
}
attribute_name_index對應十六進制為 00 09,在常量池結構表中查找結果:
#9 = Utf8 Code
從字節碼中每一個方法中都能體現出來,比如默認構造方法:
public com.dskj.jvm.bytecode.MyTest1;
descriptor: V
flags: ACC_PUBLIC
Code:
...
然后根據 atrribute_length 對應十六進制為 00 00 00 38 轉換為十進制為3 * 16的一次方 + 8 = 56
說明在這個十六進制后面找到56個字節作為Code這個屬性的具體的值。
方法表結構:
前三個字段和field_info一樣。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count]
}
方法的屬性結構:
-
JVM預定了部分atrribute,但是編譯器自己也可以實現自己的atrribute寫入class文件里,供運行時使用。
-
不同的attribute通過attribute_name_index來區分。
Code結構:
Code attribute的作用是保存該方法的結構,如所對應的字節碼。
-
attribute_length表示attribute所包含的字節數,不包含atrribute_name_index和attribute_length字段。
-
max_stack表示這個方法運行的任何時刻所能達到的操作數棧的最大深度。// 00 02
-
max_locals表示方法執行期間創建的局部變量的數目,包含用來表示傳入的參數的局部變量的。// 00 01
-
code_length表示該方法所包含的字節碼的字節數以及具體的指令碼。也即助記符。// 00 00 00 0A 轉換為十進制值為10,即跟著后面的10個字節 2A B7 00 01 2A 04 B5 00 02 B1 這些是字節碼具體指令,對應到構造方法的字節碼:
那么,這些十六進制是怎么和下面的助記符對應的呢?
我們通過jclasslib工具(字節碼查看工具,支持IDEA插件形式安裝)查看時,點擊助記符的鏈接會跳到Oracle官網可查看具體詳細解釋。第一個助記符: 0: aload_0 打開鏈接可以看到:
鏈接地址:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.aload_n
具體解釋內容所示:
aload_<n>
Operation
Load reference from local variable
Format
aload_<n>
Forms
aload_0 = 42 (0x2a) // 通過這里就能直接看到 aload_0 對應的十進制是42,轉換為十六進制就是 0x2a,對應字節碼文件中的 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>必須是一個到當前棧幀局部變量數組的一個索引,位于<n>位置上的局部變量會包含一個引用,位于<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 ...]] →
...
-
具體字節碼即是該方法被調用時,虛擬機所執行的字節碼。
-
exception_table,這里存放的是處理異常的信息。
-
每個exception_table表項由start_pc,end_pc,handler_pc,catch_type組成。
-
start_pc和end_pc表示在code數組中的從start_pc到end_pc處(包含start_pc,不包含end_pc)的指令拋出的異常會由這個表項來處理。
-
handler_pc表示處理異常的代碼的開始處。catch_type表示會被處理的異常類型,它指向常量池里的一個異常類。當catch_type為0時,表示處理所有的異常。
附加屬性
LineNumberTable:這個屬性用來表示code數組中的字節碼和Java代碼行數之間的關系。這個屬性可以用來在調試的時候定位代碼的執行行數。
LocalVariableTable:局部變量表,當前類中只有唯一的局部變量,而這個局部變量就是this當前對象。
局部變量表屬性類似于行號表屬性。
請注意:
Java源代碼角度:Java類中的實例方法中可以直接使用this。
Java字節碼角度: Java類中的非靜態方法,即實例方法中的這個this實際是通過編譯器隱示的作為方法的第一個參數傳遞進來(有點類似于Python中的方法,其方法中的第一個參數都會傳遞一個self變量,表示當前對象本身)。這樣使得每一個實例方法內部都可以很順利的訪問this。換句話說針對類的實例方法它至少會有一個LocalVariable局部變量,這個變量就是this。
4.7 字段/方法描述符
在JVM規范中,每個變量/字段都有描述信息,描述信息主要的作用是描述字段的數據類型、方法的參數列表(包括數量、類型與順序)與返回值。根據描述符規則,基本數據類型和代表無返回值的void類型都用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示。為了壓縮字節碼文件的體積,對于基本數據類型,JVM都只使用一個大寫字母來表示,如下所示:
B - byte,C - char,D - double,F - float,I - int,J - long,S - short,Z - boolean,V - void,L - 對象類型,如Ljava/lang/String;
數組類型: 針對數組類型來說,每一個維度使用一個前置的[來表示,如:
int數組被記錄[I,String[]二維數組被記錄為[[Ljava/lang/String;
方法描述符
用描述符描述方法時,按照先參數列表,后返回值的順序來描述。參數列表按照參數的嚴格順序放在一組之內,如方法:
String getInfoByIdAndName(int id, String name),該方法的描述符為:(I, Ljava/lang/String;)Ljava/lang/String;
Java字節碼文件的工具推薦:
https://github.com/ingokegel/jclasslib