1. 程式人生 > 其它 >JVM-位元組碼

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

到此剛好讀取完成