一、什么是字節碼
Java字節碼是Java虛擬機所使用的指令集,是八位字節的二進制流,數據項按順序存儲在class文件中,相鄰的項之間沒有任何間隔,這樣可以使得class文件緊湊。任何一個Class文件都對應着唯一的一個類或接口的定義信息,但是反過來說,類或接口並不一定都得定義在文件(譬如類或接口也可以動態生成,直接送入類加載器中),也就是有一些class可以不需要以磁盤文件的形式存在。
簡單的來說字節碼文件即.java文件通過javac命令生成的.class文件。
jvm運行的是.class文件 而java kotlin等語言都可以通過編譯器編譯成.class文件
jvm會把編譯的.class文件通過加載 到類加載子系統中 完成連接、初始化的步驟 成為class對象之后運行
二、Class類文件的結構
序號 | 名稱 | 意思 | 類型 | 數量 |
---|---|---|---|---|
1 | magic | 魔數 | U4 | 1 |
2 | minor_version | 次板號 | U2 | 1 |
3 | major_version | 主版本號 | U2 | 1 |
4 | constant_pool_count | 常量池大小 | U2 | 1 |
5 | constant_pool | 常量池 | - | costant_pool_count - 1 |
6 | access_flags | 類的訪問控制權限 | U2 | 1 |
7 | this_class | 類名 | U2 | 1 |
8 | super_class | 父類名 | U2 | 1 |
9 | interfaces_count | 接口數量 | U2 | 1 |
10 | interfaces[] | 實現的接口 | - | interfaces_count |
11 | fields_count | 成員屬性數量 | U2 | 1 |
12 | field_info[] | 成員屬性值 | - | fields_count |
13 | methods_count | 方法數量 | U2 | 1 |
14 | method_info[] | 方法值 | - | method_count |
15 | attributes_count | 類屬性數量 | U2 | 1 |
16 | attribute_info[] | 類屬性值 | - | attributes_count |
根據《Java虛擬機規范》的規定,Class文件格式采用一種類似於C語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型:“無符號數”和“表”
- 無符號數屬於基本數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節、8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值
- 表:由多個無符號數或者其他表作為數據項構成的復合數據類型,以命名_info結尾。
接下來我們就來一一了解Class文件各組成部分,為了更直觀的了解我們打開一個Class文件作為參照,因為class文件是16進制存儲的,我們需要用一些工具打開,不然直接打開是亂碼,我使用的是UltraEdit的軟件。
- 接下來的例子都是圍繞這個類的.class文件展開的
public class Test {
public static String a = "1";
public static final int b = 2;
public static void main(String[] args) {
System.out.println(a);
}
}
1. 魔數 U4
每個Class文件的頭4個字節被稱為魔數(Magic Number),固定值為0xCAFEBABE。魔數的作用是表示文件的類型,比如PNG圖片文件、MP4可播放文件、PDF等文件基本都有自己的特殊的魔數,第三方解析器例如瀏覽器就可以通過魔數字符識別出文件的類型然后進行對應的邏輯解析處理。
我們這里只要記住class文件的魔數數字就是cafe babe。class的魔數的作用是判斷該文件是不是一個合格class文件。
2. 次版本號 U2
占2個字節 次版本號一般全部固定為零,只有在Java2出現前被短暫使用過。但在JDK12時期,由於JDK提供的功能集已經非常龐大,有一些復雜的新特性需要以“公測”的形式放出,所以設計者重新啟用了副版本號,將它用於標識“技術預覽版”功能特性的支持。
3. 主版本號 U2
主版本號作用是區分jdk的版本。1.0的主版本號是44,8.0的版本號是52。高版本的JDK能向下兼容以前版本的Class文件,但不能運行以后版本的Class文件。
比如你是在jdk8上編譯的,你到運行環境是jdk7的上去運行,就不會讓你運行 就是用過版本號來判斷的。
注意:主版本號是34是16進制 得轉化成10進制 34轉化成10進制就是52 說明我們用的是JDK8
4. 常量池大小 U2
由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值。需要注意的是這個容量計數是從1而不是0開始。占2個字節。
注意:常量池大小是2B 轉10進制為43 說明有42項常量(容量計數是從1而不是0開始)
5. 常量池(靜態常量池)
我們的常量池可以看作我們的java class類的一個資源倉庫(比如Java類定的 方法和變量信息),我們后面的方法、類的信息的描述信息都是通過索引去常量池中獲取。常量池是表類型數據項目。
常量池主要存放兩種常量: 字面量和符號引用,字面量比較接近於Java語言層面的常量概念,而符號引用則屬於編譯原理方面的概念。
1、字面量包含:文本字符串、final常量值、基本數據類型等
2、符號引用包含:類與接口的的全類名、字段和名稱的描述符、方法名稱和描述符等
常量池有三種
1、class中的常量池 靜態的(我們這里分析的就是這個常量池 .class文件里的符號引用)
2、運行時常量池 動態的(加載或運行時把符號引用轉化為直接引用 靜態鏈接[加載階段的解析過程]和動態連接[棧幀方法調用過程中])
3、字符串常量池(jdk1.6字符串常量池是包含在運行常量池中的 jdk1.7字符串常量池從永久代里的運行時常量池分離到堆里)
常量池中每一項常量都是一個表,表結構起始的第一位是個u1類型的標志位(tag取值見表中標志列),代表着當前常量屬於哪種常量類型。
常量池的項目類型表:
常量池中的17種數據類型的結構總表:
- 讀取第一個標志位為0A 轉10進制為10 到常量池的項目類型表中查詢到代表於CONSTANT_Methodref_info類型
- 再到結構總表中查詢出它的結構
- 具體分析第一個常量:
tag:
u1占一個字節所以讀取一個
0A 轉10進制為:10 所以這個常量類型是 CONSTANT_Methodref_info
index:你對應的是哪個類的
u2占兩個字節所以讀取兩個
00 07 轉10進制為:7 calss_index符號引用值為7
index:是哪個類型的
u2占兩個字節所以讀取兩個
00 1C 轉10進制為:28 name_and_type_index符號引用值為28
#1 = Methodref #7.#28 // java/lang/Object."<init>":()V
第一個index 是7 對應#7 你對應的是哪個類的
第二個name_and_type_index 是28 對應#28
- 如何驗證我們分析的類型是否正確 可以通過idea插件jclasslib或者命令行模式去驗證:
在idea Terminal命令行中切到對應class目錄下執行命令
javap -v Test.class
就這樣依次向下分析 每分析完一個對照分析看是否正確
6. 訪問標志 U2
占兩個字節 讀取00 21 代表public
訪問標志表
7. 類名 U2
00 06 代表指向常量池 #6的地址
#6 = Class #35 // com/leetcode/test/Test
// 這個又指向#35 在常量池找到35
#35 = Utf8 com/leetcode/test/Test
最終得出類名為 com/leetcode/test/Test
8. 父類名 U2
00 07 代表指向常量池 #7的地址
#7 = Class #36 // java/lang/Object#37 = Utf8 java/lang/Object
// 這個又指向#36 在常量池找到36
#36 = Utf8 java/lang/Object
最終得出父類名為 java/lang/Object
9. 接口數量 U2
00 00 如果為0 實現接口interface[]這片區域在字節碼文件中不會出現
10. 實現接口
因為接口數量為0 沒有這片區域 跳過
11. 成員屬性數量 U2
00 02 說明成員屬性有2個
12. 成員屬性值
成員屬性的存儲結構:
u2 access_flags 權限修飾符
u2 name_index 字段名稱索引(類型名稱)
u2 descriptor_index 字段描述索引(類型)
u2 attributes_count 屬性表個數(屬性數量)
attribute_info attribute[attribute_count](屬性內容 如果屬性數量為0 則沒有)
attribute_info的存儲結構:
u2 attribute_name_index
u4 attribute_length
u1 info[attribute_length]
我們開始分析兩個成員屬性值
第一個:
u2 access_flags 00 09 代表public static
u2 name_index 00 08 指向常量池#8 #8 = Utf8 a
u2 descriptor_index 00 09 指向常量池#9 #9 = Utf8 Ljava/lang/String;
u2 attributes_count 00 00(因為屬性數量為0 屬性內容區域沒有值)
所以第一個成員屬性是 public static String a
第二個:
u2 access_flags 00 19 代表public static final(public final static也為19)
u2 name_index 00 0A 指向常量池#10 #10 = Utf8 b
u2 descriptor_index 00 0B 指向常量池#11 #11 = Utf8 I (在字節碼中I是int的簡寫 參照下面的數據類型的描述符表)
u2 attributes_count 00 01 代表有一個attribute_info
// attribute_info attribute[attribute_count] 因為屬性數量為1 所以需要讀取一個attribute_info[1]
attribute_info{
u2 attribute_name_index 00 0C 指向常量池#12 #12 = Utf8 ConstantValue
u4 attribute_length 00 00 00 02
u1 info[attribute_length] 因為屬性數量為2 所以需要讀取2個attribute_info[2] 讀取2個字節 00 0D 指向常量池#13 #13 = Integer 2
}
所以第二個成員屬性是 public static final int b = 2
驗證我們讀取的結果:
數據類型的描述符表
基本數據類型表示:
B---->byte
C---->char
D---->double
F----->float
I------>int
J------>long
S------>short
Z------>boolean
V------->void
void-------> ()v
對象類型:
String------>Ljava/lang/String;(后面有一個分號)
對於數組類型:每一個唯獨都是用一個前置 [ 來表示
int[]------>[I,
String[][]------>[[Ljava.lang.String;
byte[]------>[B
String[]------>[Ljava/lang/String
二維數組就是
byte[][]------>[[B
方法的描述符規則:()V表示: (數據類型的描述符)返回值的描述符
1、比如方法為:public static void main(String[] args) {}
方法的描述符為:([Ljava/lang/String;)V
2、比如方法描述符為:([[Ljava/lang/String;, I, [Ljava/lmw/Liu;)[Ljava/lang/String
方法就為:String xxx(String[][] str, int a, Liu liu)
虛擬機規范預定義的屬性表(這里只羅列了一部分)
13. 方法數量 U2
00 03 說明方法有3個(注意要把構造方法算進去)
14. 方法值
方法值的存儲結構:
u2 access_flags 權限修飾符
u2 name_index 字段名稱索引(類型名稱)
u2 descriptor_index 字段描述索引(類型)
u2 attributes_count 方法表個數(屬性內容)
attribute_info attribute[attribute_count](屬性內容 如果屬性數量為0 則沒有)
"attribute_info":
"Code": {
"attribute_name_index": "u2(00 09)->desc:我們屬性的名稱指向常量值索引的#9 位置 值為Code",
"attribute_length": "u4(00 00 00 2F)-desc:表示我們的Code屬性緊接着下來的47個字節是Code的內容",
"max_stack": "u2(00 01)->desc:表示該方法的最大操作數棧的深度1",
"max_locals": "u2(00 01)->desc:表示該方法的局部變量表的個數為1",
"Code_length": "u4(00 00 00 05)->desc:指令碼的長度為5",
"Code[Code_length]": "2A B4 00 02 B0 其中0x002A->對應的字節碼注記符是aload_0;0xB4->getfield 獲取指定類的實例域,並將其值壓入棧頂;
00 02表示表示是B4指令碼操作的對象指向常量池中的#2
B0表示為aretrun 返回 從當前方法返回對象引用",
"exception_table_length": "u2(00 00)->表示該方法不拋出異常,故exception_info沒有異常信息",
"exception_info": {},
"attribute_count": "u2(00 02)->desc表示code屬性表的屬性個數為2",
"attribute_info": {
"LineNumberTable": {
"attribute_name_index": "u2(00 0A)當前屬性表名稱的索引指向我們的常量池#10(LineNumberTable)",
"attribute_length": "u4(00 00 00 06)當前屬性表屬性的字段占用6個字節是用來描述line_number_info",
"mapping_count": "u2(00 01)->desc:該方法指向的指令碼和源碼映射的對數 表示一對",
"line_number_infos": {
"line_number_info[0]": {
"start_pc": "u2(00 00)->desc:表示指令碼的行數",
"line_number": "u2(00 0B)->desc:源碼12行號"
}
},
"localVariableTable": {
"attribute_name_index": "u2(00 0B)當前屬性表名稱的索引指向我們的常量池#10(localVariableTable)",
"attribute_length": "u4(00 00 00 0C)當前屬性表屬性的字段占用12個字節用來描述local_variable_info",
"local_variable_length": "u2(00 01)->desc:表示局部變量的個數1",
"local_vabiable_infos": {
"local_vabiable_info[0]": {
"start_pc": "u2(00 00 )->desc:這個局部變量的生命周期開始的字節碼偏移量",
"length:": "u2(00 05)->作用范圍覆蓋的長度為5",
"name_index": "u2(00 0c)->字段的名稱索引指向常量池12的位置 this",
"desc_index": "u2(00 0D)局部變量的描述符號索引->指向#13的位置
"index": "u2(00 00)->desc:index是這個局部變量在棧幀局部變量表中Slot的位置"
}
}
}
}
}
}
我們開始分析三個方法值
由於方法值中attribute_info信息過於的多 這里就不展開分析了
第一個:
u2 access_flags 00 01 01 代表public
u2 name_index 00 0E 14 對應常量池的#14 #14 = Utf8 <init>(構造方法)
u2 descriptor_index 00 0F 15 對應常量池的#15 #15 = Utf8 ()V
u2 attribuyes_count 00 01
attribute_info attribute[attribute_count] 因為屬性數量為1 所以需要讀取一個attribute_info[1]
....
第二個:
u2 access_flags 00 09 代表public static
u2 name_index 00 15 21 對應常量池的#21 #21 = Utf8 main
u2 descriptor_index 00 16 22 對應常量池的#22 #22 = Utf8 ([Ljava/lang/String;)V
u2 attribuyes_count 00 01
attribute_info attribute[attribute_count] 因為屬性數量為1 所以需要讀取一個attribute_info[1]
...
第三個:
u2 access_flags 00 08 代表static
u2 name_index 00 19
u2 descriptor_index 00 0F
u2 attribuyes_count 00 01
attribute_info attribute[attribute_count] 因為屬性數量為1 所以需要讀取一個attribute_info[1]
...
最后也是可以通過命令行打印出來的信息來驗證我們讀取的結果
15.類屬性數量 U2
讀取兩個 00 01
16.類屬性值
類屬性值的存儲結構:
u2 attribute_name_index
u4 name_index_length
u2 sourcefile_index
按照數據結構讀取
u2 attribute_name_index 00 1A 26對應常量池的#26 #26 = Utf8 SourceFile
u4 name_index_length 00 00 00 02
u2 sourcefile_index 00 1B 27對應常量池的#27 #27 = Utf8 Test.java
到此剛好讀取完成
三、字節碼指令
Java虛擬機的指令由一個字節長度的、代表着某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其后的零至多個代表此操作所需的參數(稱為操作數,Operand)構成。
我們可以通過idea的一個插件來查看一個類的字節碼指令
1.編譯文件
2.使用idea jclasslib 插件工具查看(安裝后在view窗口下 show bytecode with jclasslib)
3.Methods ——> main ——> code下:
public class Test {
public static String a = "1";
public static final int b = 2;
public static void main(String[] args) {
int c = 3;
System.out.println(a);
}
}
百度搜索:字節碼指令手冊 可以知道這些指令的意思
iconst_3:將 int 型 3 推送至棧頂
istore_1:將棧頂 int 型數值存入第二個本地變量
getstatic:訪問類的域和類實例域
invokevirtual:調用實例方法
return:從當前方法返回void