引言
??我們知道,使用Java編寫的類文件,在經過編譯之后,變成.class文件。Class文件是一組以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據。
??根據Java虛擬機規范的規定,Class文件格式采用一種類似于C語言結構體的偽結構來存儲數據,這種偽結構只有兩種數據類型:無符號數和表,下面的解析都要以這兩種數據類型為基礎,這里先介紹一下。
??無符號數屬于基本的數據類型,以u1、u2、u4、u8來分表代表1個字節、2個字節、4個字節、8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量之或者按照UTF-8編碼構成的字符串值等。
??表是由多個無符號數或者其他表作為數據項構成的復合數據類型,所有表都習慣性地以“_info”結尾。表用于描述有層次關系的復合解雇的數據,整個Class文件本質上就是一張表,如下圖所示:
分析前準備
??在分析Class文件結構之前,我們先來生成一個寫一個在Java中最常見的實例類,代碼如下:
public class Person {
private String name;
public int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
如上圖所以,一個最常見的Person類,有兩個屬性:name和age。又生成了各自的set和get方法。下面使用javac將該Person.java類編譯成Person.class。
javac Person.java
使用文本打開Person.class文件,如下所示:
cafe babe 0000 0034 001d 0a00 0500 1809
0004 0019 0900 0400 1a07 001b 0700 1c01
0004 6e61 6d65 0100 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 0003 6167
6501 0001 4901 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0007
6765 744e 616d 6501 0014 2829 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0773 6574 4e61 6d65 0100 1528 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 2956
0100 0667 6574 4167 6501 0003 2829 4901
0006 7365 7441 6765 0100 0428 4929 5601
000a 536f 7572 6365 4669 6c65 0100 0b50
6572 736f 6e2e 6a61 7661 0c00 0a00 0b0c
0006 0007 0c00 0800 0901 0016 636f 6d2f
7171 792f 6d61 7064 656d 6f2f 5065 7273
6f6e 0100 106a 6176 612f 6c61 6e67 2f4f
626a 6563 7400 2100 0400 0500 0000 0200
0200 0600 0700 0000 0100 0800 0900 0000
0500 0100 0a00 0b00 0100 0c00 0000 1d00
0100 0100 0000 052a b700 01b1 0000 0001
000d 0000 0006 0001 0000 0008 0001 000e
000f 0001 000c 0000 001d 0001 0001 0000
0005 2ab4 0002 b000 0000 0100 0d00 0000
0600 0100 0000 0e00 0100 1000 1100 0100
0c00 0000 2200 0200 0200 0000 062a 2bb5
0002 b100 0000 0100 0d00 0000 0a00 0200
0000 1200 0500 1300 0100 1200 1300 0100
0c00 0000 1d00 0100 0100 0000 052a b400
03ac 0000 0001 000d 0000 0006 0001 0000
0016 0001 0014 0015 0001 000c 0000 0022
0002 0002 0000 0006 2a1b b500 03b1 0000
0001 000d 0000 000a 0002 0000 001a 0005
001b 0001 0016 0000 0002 0017
除此之外,我們可以通過javap命令來查看Class文件的具體信息
javap -v -c -s -l Person.class
得到的信息如下所示:
Classfile /Users/qin/AndroidStudioProjects/MapDemo/app/src/main/java/com/qqy/mapdemo/Person.class
Last modified 2020-12-21; size 556 bytes
MD5 checksum c03fc3c9928ccc5beba4b52b5aba890e
Compiled from "Person.java"
public class com.qqy.mapdemo.Person
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#25 // com/qqy/mapdemo/Person.name:Ljava/lang/String;
#3 = Fieldref #4.#26 // com/qqy/mapdemo/Person.age:I
#4 = Class #27 // com/qqy/mapdemo/Person
#5 = Class #28 // java/lang/Object
#6 = Utf8 name
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 age
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 getName
#15 = Utf8 ()Ljava/lang/String;
#16 = Utf8 setName
#17 = Utf8 (Ljava/lang/String;)V
#18 = Utf8 getAge
#19 = Utf8 ()I
#20 = Utf8 setAge
#21 = Utf8 (I)V
#22 = Utf8 SourceFile
#23 = Utf8 Person.java
#24 = NameAndType #10:#11 // "<init>":()V
#25 = NameAndType #6:#7 // name:Ljava/lang/String;
#26 = NameAndType #8:#9 // age:I
#27 = Utf8 com/qqy/mapdemo/Person
#28 = Utf8 java/lang/Object
{
public int age;
descriptor: I
flags: ACC_PUBLIC
public com.qqy.mapdemo.Person();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 14: 0
public void setName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 18: 0
line 19: 5
public int getAge();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #3 // Field age:I
4: ireturn
LineNumberTable:
line 22: 0
public void setAge(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #3 // Field age:I
5: return
LineNumberTable:
line 26: 0
line 27: 5
}
SourceFile: "Person.java"
Class文件結構
??在以上準備工作做好之后,我們來正式開始分析Person.class中十六進制的數據是怎么排列的,以及各個部分都是如何表示類信息的。
魔數
每一個字節碼文件的開頭都是一個確定的占四個字節的16進制的數字,我們把這個叫做魔數,值為:cafe babe。這個值是確定的,每一個.class文件的開頭都必須是該值。
Java版本
魔數之后是Java的版本信息,也是占四個字節,其中前兩個字節是次版本,后兩個字節是主版本。這里的值是:0000 0034,轉換為10進制,對應的Java版本中,次版本是0,主版本是52(1.8),所以Java版本是1.8.0,我們看下本機的Java版本:
java version "1.8.0_131"
常量池
緊接著是常量池,常量值的所占長度是不確定的,其中前兩個字節是常量池的長度,001d,對應的十進制是 16 + 13 - 1 = 28,為什么要減一呢?因為0是被JVM虛擬機占用的,我們看下被我們通過javap命令得出的字節碼文件結構中的常量池部分:
Constant pool:
#1 = Methodref #5.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#25 // com/qqy/mapdemo/Person.name:Ljava/lang/String;
#3 = Fieldref #4.#26 // com/qqy/mapdemo/Person.age:I
#4 = Class #27 // com/qqy/mapdemo/Person
#5 = Class #28 // java/lang/Object
#6 = Utf8 name
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 age
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 getName
#15 = Utf8 ()Ljava/lang/String;
#16 = Utf8 setName
#17 = Utf8 (Ljava/lang/String;)V
#18 = Utf8 getAge
#19 = Utf8 ()I
#20 = Utf8 setAge
#21 = Utf8 (I)V
#22 = Utf8 SourceFile
#23 = Utf8 Person.java
#24 = NameAndType #10:#11 // "<init>":()V
#25 = NameAndType #6:#7 // name:Ljava/lang/String;
#26 = NameAndType #8:#9 // age:I
#27 = Utf8 com/qqy/mapdemo/Person
#28 = Utf8 java/lang/Object
這部分非常重要,因為在下面的方法、類信息、屬性等等的分析 中,都會用到這部分的內容,下面我們花點時間,把上述的28個常量挨個分析一遍。
分析這部分的內容,我們需要用到一個前面提到的表結構說明,如下圖所示:
常量1
tag值為0a,轉換為10進制是10,因此類型是:CONSTANT_Methodref_info。接著是兩個占兩個字節的index,0005 0018,分別對應05#和24#,我們看下圖3:
#1 = Methodref #5.#24 // java/lang/Object."<init>":()V
至于后面的信息,我們稍后再介紹。可以看到,通過javap命令得到的信息驗證了我們分信息得出的結論。
常量2
tag值為09,類型為:CONSTANT_Fieldref_info,接著兩個index,0004 0019,分別對應04#和25#,表示字段name。
#2 = Fieldref #4.#25 // com/qqy/mapdemo/Person.name:Ljava/lang/String;
常量3
tag值為09,類型為:CONSTANT_Fieldref_info,接著兩個index,0004 001a ,分別對應#04和#26,表示字段age。
#3 = Fieldref #4.#26 // com/qqy/mapdemo/Person.age:I
常量4
tag值為07,類型為:CONSTANT_Class_info,接著是兩個字節的index,值為:001b,對應索引值為:#27,表示類信息
#4 = Class #27 // com/qqy/mapdemo/Person
常量5
tag值為07,類型為:CONSTANT_Class_info,接著是兩個字節的index,值為:001c,對應索引值為#28,表示類信息
#5 = Class #28 // java/lang/Object
####### 常量6
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 04。這里的長度是4,我們往后數四個字節的數據,6e 61 6d 65,對應ASCII表,值為:name,對應字段名稱。
#6 = Utf8 name
常量7
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 12。這里的長度是18,我們往后數18個字節的數據,4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b,對應ASCII表,值為:Ljava/lang/String;
#7 = Utf8 Ljava/lang/String;
常量8
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 03。這里的長度是3,我們往后數3個字節的數據,61 67 65,對應ASCII表,值為:age,對應字段名稱。
#8 = Utf8 age
常量9
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 01。這里的長度是1,我們往后數1個字節的數據,49,對應ASCII表,值為:I,標識Integer。
#9 = Utf8 I
常量10
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 06。這里的長度6,我們往后數6個字節的數據,3c 69 6e 69 74 3e ,對應ASCII表,值為:<init>,表明是構造方法。
#10 = Utf8 <init>
常量11
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 03。這里的長度6,我們往后數3個字節的數據:28 29 56 ,對應ASCII表,值為:()V,表明構造方法的沒有入參,返回值是Void。這是JVM默認給加的默認的構造方法。
常量12
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 04。這里的長度4,我們往后數4個字節的數據:43 6f 64 65 ,對應ASCII表,值為:Code。這里的Code很重要,在Code里面存放的是JVM指令集。
#12 = Utf8 Code
常量13
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 0f。這里的長度15,我們往后數15個字節的數據:4c 69
6e 65 4e 75 6d 62 65 72 54 61 62 6c 65,對應ASCII表,值為:LineNumberTable。代表的意思是行號表,表示JVM指令和Java代碼的映射關系,JVM在執行指令的時候,如果有異??梢跃珳识ㄎ坏絁ava代碼中,是因為這個。
#13 = Utf8 LineNumberTable
常量14
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 07。這里的長度7,我們往后數7個字節的數據:67 65 74 4e 61 6d 65,對應ASCII表,值為:getName,表示方法名。
#14 = Utf8 getName
常量15
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 14。這里的長度20,我們往后數20個字節的數據:28 29 4c 6a 61 76
61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b ,對應ASCII表,值為:()Ljava/lang/String;,表示方法沒有入參,返回值是String類型。
#15 = Utf8 ()Ljava/lang/String;
常量16
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 07。這里的長度7,我們往后數7個字節的數據:07 73 65 74 4e 61 6d 65 ,對應ASCII表,值為:setName,表示setName方法。
#16 = Utf8 setName
常量17
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 15。這里的長度21,我們往后數21個字節的數據:28 4c 6a 61 76
61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 ,對應ASCII表,值為:(Ljava/lang/String;)V。這里表示方法的入參是String類型,返回值是Void類型。
#17 = Utf8 (Ljava/lang/String;)V
常量18
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 06。這里的長度6,我們往后數6個字節的數據:67 65 74 41 67 65 ,對應ASCII表,值為:getAge,表示getAge方法。
#18 = Utf8 getAge
常量19
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 03。這里的長度3,我們往后數3個字節的數據:28 29 49 ,對應ASCII表,值為:()I,表示方法沒有入參,返回值是Integer類型。
#19 = Utf8 ()I
常量20
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 06。這里的長度6,我們往后數6個字節的數據:73 65 74 41 67 65 ,對應ASCII表,值為:setAge,表示setAge方法。
#20 = Utf8 setAge
常量21
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 04。這里的長度4,我們往后數4個字節的數據:28 49 29 56,對應ASCII表,值為:(I)V,表示方法的入參是Integer類型,返回值是Void。
#21 = Utf8 (I)V
常量22
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 0a。這里的長度10,我們往后數10個字節的數據:53 6f 75 72 63 65 46 69 6c 65 ,對應ASCII表,值為:SourceFile。表示源文件。
#22 = Utf8 SourceFile
常量23
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 0b。這里的長度11,我們往后數11個字節的數據:50 65 72 73 6f 6e 2e 6a 61 76 61 ,對應ASCII表,值為:Person.java。表示源文件是Persion.java。
#23 = Utf8 Person.java
常量24
tag為0c,類型為:CONSTANT_NameAndType_info,接著是長度,占四個字節,值為:00 0a 00 0b 。表示是#10和#11。
#24 = NameAndType #10:#11 // "<init>":()V
常量25
tag為0c,類型為:CONSTANT_NameAndType_info,接著是長度,占四個字節,值為:00 06 00 07。表示#06和#07。
#25 = NameAndType #6:#7 // name:Ljava/lang/String;
常量26
tag為0c,類型為:CONSTANT_NameAndType_info,接著是長度,占四個字節,值為:00 08 00 09。表示#08和#09。
#26 = NameAndType #8:#9 // age:I
常量27
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 16。這里的長度22,我們往后數22個字節的數據:63 6f 6d 2f
71 71 79 2f 6d 61 70 64 65 6d 6f 2f 50 65 72 73
6f 6e ,對應ASCII表,值為:com/qqy/mapdemo/Person。表示類所在的Package。
#27 = Utf8 com/qqy/mapdemo/Person
常量28
tag值為01,類型為:CONSTANT_Utf8_info,接著是長度,占兩個字節:00 10。這里的長度16,我們往后數16個字節的數據:6a 61 76 61 2f 6c 61 6e 67 2f 4f
62 6a 65 63 74 ,對應ASCII表,值為:java/lang/Object。
#28 = Utf8 java/lang/Object
到這里,常量池的部分已經分析完了。在分析每一個常量的時候,各自代表的意思已經說得很清楚了,這里就不再一一贅述了。
Access Flags
通過字節碼結構圖我們知道,常量池下面是Access Flags,即訪問標志,占兩個字節,值為:00 21??聪旅嬉粡垐D:
我們可以看到,圖里面并沒有Ox0021,這是因為該值是0x0020和0x0001的并集,即ACC_PUBLIC和ACC_SUPER。這里標識該類是public的,并且可以訪問父類。
This Class Name
訪問標識之后是類名,占兩個字節,值為:00 04。這里表示指向常量池中#04的信息,我們看上面的常量池的#4值,為:com/qqy/mapdemo/Person。
Super Class Name
This Class Name后面是Super Class Name,即父類的名字。占兩個字節,值為:00 05。這里標識指向常量池中#05的信息,我們看上面的常量池的#05的值
#5 = Class #28 // java/lang/Object
值為:java/lang/Object。
Interfaces
接口,這里占的2 + n個字節,包括兩部分,第一部分是interfaces_count(接口的個數),第二部分是interfaces(接口名)。我們來看第一部分的兩個字節,值為:00 00。表示接口個數為0,因此該類沒有實現任何接口,所以下面的interfaces就不存在了。如果count大于0的話,那接口表是存在的。有興趣的同學可以自行去實現一下看卡。
我們知道,在Java中,一個類最多實現的接口個數是65535,那么這個65535是怎么來的呢?答案就在這里,接口個數占2個字節,也就是16位,如果16位全部是1,那就是65535,所以最大值是65535。
Fields
字段表。跟上面的接口一樣,也是占2 + n個字節,包括兩部分。其中第一部分是字段表的長度,占兩個字節,值為:00 02,表示有兩個字段。這里就是我們的name字段和age字段了。
后面接跟著是filed_info[],是一個描述字段的數組。filed_info的結構是什么樣呢?如下圖所示:
再來看下字段訪問標志,如下圖:
如圖4所示,每一個字段需要訪問標志、字段名稱和字段類型來唯一確定。 attribute_count標識屬性表長度,attribute_info表示字段屬性。
我們接著分析,字段的訪問標志占兩個字節,我們接著數兩個字節:0002。通過圖5我們可以得到0x0002代表的是ACC_PRIVATE,是私有的。
接著往下,下面是字段名稱索引,占兩個字節,值為:00 06,都應常量池中的#06,我們去上面常量池中可以知道,#06對應的是name,即字段名稱是name。
再接著往下,是字段索引描述,同樣占兩個字符,值為:00 07,對應的是#07,查常量值知道,對應的是:Ljava/lang/String,即字段是String類型的。
接著再往下,屬性表個數,占兩個字節,值為:00 00。因此沒有屬性表。該字段到此就結束了??偨Y起來就是,私有的、類型為String類型的、名稱為name的字段,即我們java代碼中的:
private String name;
接著往下,下一個字段的訪問標志位占兩個字節, 00 01,查表為:ACC_PUBLIC,表示公有。
接著兩個字節為字段名稱索引,占兩個字節,值為:00 08,對應的是#08,查常量值知道,對應的是:age。表示字段名稱是age。
再往下兩個字節為字段的索引描述,同樣占兩個字符,值為:00 09,對應的是#9,查常量值知道,對應的是:I,即為int類型。
再往下兩個字節,表示字段屬性長度,值為:00 00。所以這里沒有屬性。該字段到此結束,綜上為:
private int age;
Methods
方法,占用2 + n個字節,其中前兩個字節表示方法的個數,值為:00 05。說明有五個方法,我們來數一下:setName/getName/setAge/getAge/構造方法,正好五個。接下來是方法表,方法表的結構比較復雜,我們來一一看下。
老規矩,先來看下方法表結構:
接著再來看下,方法的訪問標志:
方法-1
前兩個字節為方法訪問權限,占兩個字節,值為:00 01,查表可知,為:ACC_PUBLIC,表示公有的。
接下來的兩個字節表示方法名稱的索引,值為00 0a,十進制為10,看下常量池中#10對應的值為 <init>,表示構造方法。
接著看,還是兩個字節,表示方法描述索引,值為:00 0b,十進制為11,看下常量池中#11對應的值為:()V,說明構造方法是無入參,返回值Void。
再往后的兩個字節表示方法的屬性個數,值為:00 01,表示有一個屬性。
接下來看屬性信息,先看下屬性表結構:
兩個字節,表示屬性的名稱指向常量池的索引,值為:00 0c,十進制為12,看下常量池對應的#12的數據為Code。這個Code很重要,表示字節碼指令的信息,說明此屬性是方法的字節碼描述。
接下來的四個字節,為Code內容的長度,值為:00 00 00 1d,十進制為29,表示Code長度為29。詳情為:
00 01 00 01 00 00 00 05 2a b7 00 01 b1
00 00 00 01 00 0d 00 00 00 06 00 01 00 00 00 08
我們看下Code對應的屬性結構:
上面我們已經分析了attribute_name_index和attribute_length,因此Code長度的29個字節是從max_stack開始的。
max_stack,方法最大操作數棧的深度,占兩個字節,值為:00 01,十進制為1,表示最大操作數棧的深度為1。
max_locals,方法的局部變量表的個數,占兩個字節,值為00 01,十進制為1,表示局部變量表的個數為1。
Code_length,指令碼的長度,表示有該方法對應多少個指令。占四個字節,值為:00 00 00 05,十進制為5,說明有五個指令碼。
Code[Code_length],具體指令碼,因為有五個指令碼,所以長度為5個字節,值為:2a b7 00 01 b1。
編號 | 具體指令(助記符) |
---|---|
0 | aload_0 |
1 | invokespecial #1 |
4 | return |
這里需要說明一下,機器能夠認識的是指令碼,也就是我們拿到的十六進制的數據,至于上述的aload_0或者invokespecial等等,我們稱之為助記符。
先來來看下,這個指令碼對應的分別是什么。
這里給大家推薦一個插件,名字叫Jclasslib,通過這個插件我們可以看到具體的指令信息。如圖所示:
在紅框中的信息我們可以點擊,會自動打開瀏覽器,我們看下具體信息:
可以看到,aload_0對應的十六進制是0x2a。接著往下看,點擊invokespecial:
可以看到,invokespecial對應的十六進制是0xb7。invokespecial的意思是調用父類的構造方法,那么具體是哪個父類的?這就是b7后面后面跟著的00 01決定的,00 01對應的常量池索引是#1,我們看下,在常量池中#1對應的是java/lang/Object <init>:()V,這里的意思就是Object類的構造方法。
我們再點開return看一下:
可以看到,return對應的值是0xb1。
虛擬機在讀到字節碼區域的長度后,按照順序依次讀入緊隨的5個字節,并根據字節碼指令表翻譯出所對應的字節碼指令。翻譯"2A B7 00 0A B1"的過程為:
1.讀入2A,查表得0x2A對應的指令為aload_0,這個指令的含義是將第0個Slot中為reference類型的本地變量推送到操作數棧頂。
2.讀入B7,查表得B7對應的指令為invokespecial,這條指令的作用是以棧頂的reference類型的數據所指向的對象作為方法接收者,調用此對象的實例構造器方法、private方法或者它的父類的方法。這個方法有一個u2類型的參數說明具體調用哪一個方法。
3.讀入00 0A,這是invokespecial的參數,查常量池可以為實力構造器<init>方法的符號引用。
4.讀入B1,查表得0xB1對應的指令為return,含義是返回此方法,并且返回值為Void。這條指令執行后,當前方法結束。
接下來是異常表長度,占兩個字節,值為:00 00,長度是0,表示該方法不會拋出異常。
接著往下,兩個字節00 01表示屬性表長度為1。
兩個字節是常量池索引,00 0d,十進制為13,我們看下具體值為:LineNumberTable,表示字節碼指令和java代碼行數的映射。下圖表示LineNumberTable的屬性結構:
在圖中的line_number_table是一個數量為line_number_table_length、類型為line_number_info的集合,line_number_info表包括了start_pc和line_number兩個u2類型的數據項,前者是字節碼行號,后者是Java源碼行號。因此我們可以得出,在00 0d之后的兩個字節表示的是line_number_table_length,值為:00 01,表示line_number_table的長度為1,即00 00 代表字節碼行號, 00 08代表Java源碼行號。
到這里,構造方法已經分析結束了。這里要注意一點的是,javap輸出的arg_size的值可能會有疑問,構造方法是沒有入參的啊,這里的長度怎么會是1呢?而且不管是在參數列表里面還是方法體內,都沒有定義任何局部變量,那locals怎么也是1呢?這是因為,在任何實例方法里面,都可以通過this關鍵字訪問到此方法所屬的對象。這個訪問機制對Java程序的編寫很重要,而它的實現卻非常簡單,僅僅是通過javac編譯器編譯的時候把對this關鍵字的訪問轉變為對一個普通方法參數的訪問,然后在虛擬機調用實例方法時自動傳入此參數而已。因此在實例方法的局部變量表中至少會存在一個指向當前對象實例的局部變量,局部變量表中也會預留出第一個Slot位來存放對象實例的引用。
方法-2
第二個方法為getName,我們看下getName的javap信息:
前兩個字節00 01表示方法訪問標志,查表可知為ACC_PUBLIC,對應為public。
接下來,00 0e,十進制為14,代表常量池索引#14,值為:getName。
00 0f,表示方法描述的索引,十進制為15,代表常量池#15,值為:()Ljava/lang/String;,表示方法沒有入參,返回值是String。
接下來是:00 01,代表方法的屬性個數,值為1。說明只有一個屬性。
接下來的兩個字節,00 0c,表示屬性的名稱指向常量池的索引,十進制為12,看一下常量池#12,值為:Code。表示的是Code屬性。
接下來的四個字節表示Code內容的長度,值為:00 00 00 1d,十進制為:29。
這里把數據直接列到這里,方便下面查看:00 01 00 01 00 00 00 05 2a b4 00 02 b0 00 00 00 01 00 0d 00 00 00 06 00 01 00 00 00 0e
00 01 表示方法的最大操作數棧的深度,值為1
00 01 表示局部變量表的個數,值為1
00 00 00 05 表示指令碼的長度,為5
2a b4 00 02 b0為指令碼,其中2a表示aload_0,b4表示getField,其中00 02表示常量池中#2,值為:name。areturn表示返回常量池中的#2對應的值,即返回name。
00 00表示異常信息的大小是0,因此沒有異常信息。
00 01表示屬性表的大小是1
00 0d表示是常量池中的#13,值為:LineNumberTable。
接著的四個字節是屬性的長度,00 00 00 06,表示長度為6。
接著的兩個字節表示行號表的長度,00 01,長度為1。
接下來的四個字節,前兩個字節代表字節碼行號,后兩個自己代表Java源碼行號。00 00表示aload_0,00 0e表示Java類中的第14行
return name;
方法-3
第三個方法是setName,我們看下setName的javap信息:
前兩個字節00 01表示方法訪問標志,查表可知為ACC_PUBLIC,對應為public。
接下來的兩個字節00 10表示方法名字,對應常量池中的#16,為:setName。
再往下兩個字節00 11表示方法描述索引,對應常量池中#17,為: (Ljava/lang/String;)V,表示方法的入參是String類型,返回值為Void。
再往下兩個字節00 01表示屬性表的長度,值為1。
接下來的兩個字節00 0c表示屬性值的索引,對應12,值為:Code。說明這唯一的一個屬性是Code屬性,接下來轉到Code屬性表中。
再往下四個字節表示Code屬性表的長度,值為:00 00 00 22,轉為十進制為:2 * 16 + 2 = 34,往后數34個字節,該方法結束。值為:
00 02 00 02 00 00 00 06 2a 2b b5 00 02 b1 00 00 00
01 00 0d 00 00 00 0a 00 02 00 00 00 12 00 05 00 13
00 02為max_stack,表示方法的最大操作數棧的深度,值為2
00 02位max_locals,表示局部變量表的個數,值為2
00 00 00 06表示字節碼指令的個數,長度為6,為:2a 2b b5 00 02 b1。對應:
編號 | 具體指令(助記符) |
---|---|
0 | aload_0 |
1 | aload_1 |
2 | putfield #2 <com/qqy/mapdemo/Person.name> |
5 | return |
接下來的00 00,表示異常表的個數是0,跳過。
00 01表示屬性表的大小為1,再往下取兩個字節00 0d,表示具體的屬性名稱,對應常量池中的#13,為:LineNumberTable。
接下來轉到LineNumberTable屬性結構中。
00 00 00 0a 00 02 00 00 00 12 00 05 00 13
00 00 00 0a表示屬性長度,值為:10,正好對應后面的10個字節。
00 02表示line_number_table_length,屬性個數為2,對應關系為
字節碼 | Java源碼行數 |
---|---|
00 00-0 | 00 12-18 |
00 05-5 | 00 13-19 |
方法-4
第四個方法是getAge,我們看下getAge的javap信息:
前兩個字節00 01表示方法訪問標志,查表可知為ACC_PUBLIC,對應為public。
接下來的兩個字節00 12表示方法名字,對應常量池中的#18,為:getAge。
再往下兩個字節00 13表示方法描述索引,對應常量池中#19,為: ()I
,表示方法沒有入參,返回值為int類型。
再往下兩個字節00 01表示屬性表的長度,值為1。
接下來的兩個字節00 0c表示屬性值的索引,對應12,值為:Code。說明這唯一的一個屬性是Code屬性,接下來轉到Code屬性表中。
再往下四個字節表示Code屬性表的長度,值為:00 00 00 1d,轉為十進制為:1 * 16 + 13 = 29,往后數29個字節,該方法結束。值為:
00 01 00 01 00 00 00 05 2a b4 00 03 ac
00 00 00 01 00 0d 00 00 00 06 00 01 00 00 00 16
00 01為max_stack,表示方法的最大操作數棧的深度,值為1
00 01位max_locals,表示局部變量表的個數,值為1
00 00 00 05表示字節碼指令的個數,長度為5,為:2a b4 00 03 ac 。對應:
編號 | 具體指令(助記符) |
---|---|
0 | aload_0 |
1 | getfield #3 <com/qqy/mapdemo/Person.age> |
4 | ireturn |
接下來的00 00,表示異常表的個數是0,跳過。
00 01表示屬性表的大小為1,再往下取兩個字節00 0d,表示具體的屬性名稱,對應常量池中的#13,為:LineNumberTable。
接下來轉到LineNumberTable屬性結構中。
00 00 00 06 00 01 00 00 00 16
00 00 00 06表示屬性長度,值為:6,正好對應后面的6個字節。
00 01表示line_number_table_length,屬性個數為1,對應關系為
字節碼 | Java源碼行數 |
---|---|
00 00-0 | 00 16-22 |
方法-5
第三個方法是setAge,我們看下setAge的javap信息:
前兩個字節00 01表示方法訪問標志,查表可知為ACC_PUBLIC,對應為public。
接下來的兩個字節00 14表示方法名字,對應常量池中的#20,為:setAge。
再往下兩個字節00 15表示方法描述索引,對應常量池中#21,為: (I)V,表示方法的入參是int類型,返回值為Void。
再往下兩個字節00 01表示屬性表的長度,值為1。
接下來的兩個字節00 0c表示屬性值的索引,對應12,值為:Code。說明這唯一的一個屬性是Code屬性,接下來轉到Code屬性表中。
再往下四個字節表示Code屬性表的長度,值為:00 00 00 22,轉為十進制為:2 * 16 + 2 = 34,往后數34個字節,該方法結束。值為:
00 02 00 02 00 00 00 06 2a 1b b5 00 03 b1 00 00 00
01 00 0d 00 00 00 0a 00 02 00 00 00 1a 00 05 00 1b
00 02為max_stack,表示方法的最大操作數棧的深度,值為2
00 02位max_locals,表示局部變量表的個數,值為2
00 00 00 06表示字節碼指令的個數,長度為6,為:2a 1b b5 00 03 b1。對應:
編號 | 具體指令(助記符) |
---|---|
0 | aload_0 |
1 | iload_1 |
2 | putfield #3 <com/qqy/mapdemo/Person.age> |
5 | return |
接下來的00 00,表示異常表的個數是0,跳過。
00 01表示屬性表的大小為1,再往下取兩個字節00 0d,表示具體的屬性名稱,對應常量池中的#13,為:LineNumberTable。
接下來轉到LineNumberTable屬性結構中。
00 00 00 0a 00 02 00 00 00 1a 00 05 00 1b
00 00 00 0a表示屬性長度,值為:10,正好對應后面的10個字節。
00 02表示line_number_table_length,屬性個數為2,對應關系為
字節碼 | Java源碼行數 |
---|---|
00 00-0 | 00 1a-26 |
00 05-5 | 00 1b-27 |
到這里,五個方法已經全部分析完成了。重新回到Class類結構表中,可以看到接下來的兩個字節表示attributes_count,值為:00 01,表示還有一個屬性。
再往下看兩個字節,00 16,對應的是常量池中的#22,值為:SourceFile?;氐綄傩员斫Y構圖8中查看,接下來四個字節表示屬性的長度,00 00 00 02表示長度為2個字節,值為:00 17,對應從常量表中的#23,值為:Person.java。
結語
??到這里,這篇文章就要結束了。作為字節碼分析的第一篇文章,本文給大家介紹了整體Class文件結構,以及結構表中的每一部分代表了什么,并通過一個簡單的Person類帶大家進行了一次整體的分析。當然了,字節碼文件中還有許多是我們沒有講到的,比如:我們在Person類中的方法中,異常信息都是 00 00,那如果有異常的情況是什么呢?再比如,我們遇到的屬性都是Code屬性,那么其他的屬性呢?等等......這些問題,會在后續的文章中逐一為大家講解。
bye~
參考資料
深入理解Java虛擬機-JVM高級特性與最佳實踐(第2版)