JVM-位元組碼
一、什麼是位元組碼
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. 常量池(靜態常量池)
我們的常量池可以看作我們的javaclass類的一個資源倉庫(比如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
到此剛好讀取完成