一篇帶給你JVM 字節碼解析過程
概述
概述本文主要是基于 .class 文件,進行分析 .class 文件的內容。
這部分個人覺得主要是屬于設計機構拓展的內容,大家可以一起來學習一下 Java 字節碼的設計結構以及感受一下設計者的設計。
class 類文件結構
Java 提供 javap 命令可以分析字節碼文件,我們可以使用 javap -verbose 命令分析一個字節碼文件時, 將會分析該字節碼文件的魔數、版本號、常量池、類信息、類的構造方法、類中的方法信息、類變量與成員變量等信息。
一個簡單的 Java 代碼
- public class TestClass {
- private int m;
- public int inc() {
- return ++m;
- }
- }
下圖顯示的是 Java 代碼編譯后 .class 文件的十六進制信息
bytecode_十六進制.png
為了方便對比我執行一下 javap -v TestClass
- Classfile /../../TestClass.class
- Last modified 2021-2-6; size 306 bytes
- MD5 checksum eeba40cc40cc28ef4d416ff70d901561
- Compiled from "TestClass.java"
- public class cn.edu.cqvie.jvm.bytecode.TestClass
- minor version: 0
- major version: 52
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #4.#15 // java/lang/Object."<init>":()V
- #2 = Fieldref #3.#16 // cn/edu/cqvie/jvm/bytecode/TestClass.m:I
- #3 = Class #17 // cn/edu/cqvie/jvm/bytecode/TestClass
- #4 = Class #18 // java/lang/Object
- #5 = Utf8 m
- #6 = Utf8 I
- #7 = Utf8 <init>
- #8 = Utf8 ()V
- #9 = Utf8 Code
- #10 = Utf8 LineNumberTable
- #11 = Utf8 inc
- #12 = Utf8 ()I
- #13 = Utf8 SourceFile
- #14 = Utf8 TestClass.java
- #15 = NameAndType #7:#8 // "<init>":()V
- #16 = NameAndType #5:#6 // m:I
- #17 = Utf8 cn/edu/cqvie/jvm/bytecode/TestClass
- #18 = Utf8 java/lang/Object
- {
- public cn.edu.cqvie.jvm.bytecode.TestClass();
- 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 3: 0
- public int inc();
- descriptor: ()I
- flags: ACC_PUBLIC
- Code:
- stack=3, locals=1, args_size=1
- 0: aload_0
- 1: dup
- 2: getfield #2 // Field m:I
- 5: iconst_1
- 6: iadd
- 7: dup_x1
- 8: putfield #2 // Field m:I
- 11: ireturn
- LineNumberTable:
- line 8: 0
- }
- SourceFile: "TestClass.java"
Java 字節碼結構
bytecode_結構.png
1. 魔數和 Class 文件版本
魔數:所有的.class 字節碼文件的前4個字節都是魔數,魔數為固定值: 0xCAFEBABE
版本信息,魔數之后的4個字節是版本信息,前兩個字節表示 minor version (次版本號), 后2個字節表示major version (主版本號)。這里的版本號 00 00 00 34 換算成十進制表, 表示次版本號為0, 主版本號為 52. 所以該文件的版本號為 1.8.0。可以通過 java -version 來驗證這一點。
- ➜ ~ java -version
- java version "1.8.0_281"
- Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
- Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
2. 常量池
常量池 (constant pool): 2+N個字節 緊接著主版本號之后的就是常量池入口。一個java 類中定義的很多信息都是由常量池來描述的,可以將常量池看作是 Class 文件的資源倉庫,比如說Java類中變量的方法與變量信息,都是存儲在常量池中。常量池中主要存儲2類常量:字面量與符號引用。
- 字面量, 如字符串文本,java 中聲明為final 的常量值等。
- 符號引用, 如類和接口的全局限定名, 字段的名稱和描述符,方法的名稱和描述符等。
常量池的總體結構:Java類所對應的常量池主要由常量池(常量表)的數量與常量池數組這兩部分共同構成。常量池中常量數量緊跟著在主版本號后面,占據2字節: 常量池長度 比如這里我們的十六進制就是 00 13 代表有 18 個常量。常量數組則緊跟著常量池數量之后。常量池數組與一般數組不同的是, 常量池數組中不同的元素的類型,結構都是不同的。長度當然也就不同;但是, 一種元素的第一種元素的第一個數據都是一個u1類型, 該字節是一個標識位,占據1個字節。JVM在解析常量池時,會更具這個u1 類型來獲取元素的具體類型。值得注意的是:常量池中元素的個數 = 常量池數 -1 (其中0暫時不適用), 目的是滿足某些常量池索引值的數據在特定情況下需要表達【不引用任何一個常量池】的含義:根本原因在于,索引為0也是一個常量(保留常量),只不過他不位于常量表中。這個常量就對應null值, 所以常量池的索引是從1開始而非0開始。
上面表中描述了11種數據類型的機構, 其實在jdk1.7之后又增加了3種(CONSTANT_MethodHandle_info, CONSTANT_MethodType_info 以及CONSTANT_InvokeDynami_info)。這樣一共14種。
在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[][]被表示為 [[java/lang/String;
用描述符描述方法時, 按照先參數列表, 后返回值的順序來描述. 參數列表按照參數的嚴格順序放在一組()內, 如方法: String getRealnameByIdNickname(int id, String name)的描述符為: (I, Ljava/lang/String;) Ljava/lang/String
Class 字節碼中有兩種數據類型
- 字節數據直接量:這是基本的數據類型。共細分為u1、u2、u4、u8四種,分別代表連續的1個字節、2個字節、4個字節、8個字節組成的整體數據。
- 表(數組):表示由多個基本數據或其他表,按照既定順序組成的大的數據集合。表是有結構的 。它的結構體現在:組成表的成分所在的位置和順序都是 嚴格定義好的。
常量池常量
- 0A 00 04 00 0F method_info 00 04 指向常量池中的常量的位置, 00 0F 指向的是 15 的位置。
- 09 00 03 00 10 field_info 00 03 指向 3 的位置 00 10 (16 位置)
- 07 00 11 class info 00 11 (17 位置)
- 07 00 12 class info 00 12 (18 位置)
- 01 00 01 6D utf8 長度為 1 內容為 6D 表示內容轉換為 10 進制為 109 轉換為 ASCII 碼最后的結果為 m
- 01 00 01 49 utf8 長度為 1 內容為 I
- 01 00 06 3C 69 6E 69 74 3E utf8 長度為 8 內容為
- 01 00 03 28 29 56 utf8 長度為 3 內容為:()V
- 01 00 04 43 6F 64 65 utf8長度為 4 內容為 Code
- 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 utf8 長度為 15 內容為 LineNumberTable
- 01 00 03 69 6E 63 utf8 長度為3 內容為 inc
- 01 00 03 28 29 49 長度為 3 內容為 ()I
- 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 長度為 10 內容為 SourceFile
- 01 00 0E 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61 長度為 14 內容為 TestClass.java
- 0C 00 07 00 08 NameAndType 內容 指向 7 位置。00 08 (8 位置)
- 0C 00 05 00 06 NameAndType 內容 指向 5 位置。00 06 (6 位置)
- 01 00 23 63 6E 2F 65 64 75 2F 63 71 76 69 65 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 54 65 73 74 43 6C 61 73 73 長度 35 內容 cn/edu/cqvie/jvm/bytecode/TestClass
- 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 長度 16 內容 java/lang/Object
3. 訪問標志
Access_Flag 訪問標志 訪問標識信息包括該Class文件時類和接口是否被定義成了public,是否是 abstract, 如果是類,是否被申明為成final。通過扇面的源代碼。
0x 00 21: 表示是0x0020 和0x0001的并集, 表示 ACC_PUBLIC 與 ACC_SUPER
bytecode_訪問標志.png
4. 類索引、父類索引
00 03 類名, 03 常量池位置 cn/edu/cqvie/jvm/bytecode/TestClass
00 04 父類名. 04 常量池位置 java/lang/Object
00 00 接口個數, 0 個, 接口個數最多 65535
5. 字段表集合
00 01 字段的個數, 這里有一個
bytecode_字段表.png
字段表結構
- field_info {
- u2 access_flags; 00 02 Fieldref
- u2 name_index; 00 05 (表示字段名稱在常量池中的索引位置) m
- u2 descriptor_index; 00 06 (描述符索引) I
- u2 attributes_count; 00 00
- attribute_info attributes[attributes_count]
- }
6. 方法表集合
00 02 表示有兩個方法
bytecode_方法表.png
方法表結構
- method_info {
- u2 access_flags; 00 01 Methodref
- u2 name_index; // 00 07 <init>
- u2 descriptor_index; 00 08 // ()V
- u2 attributes_count; 00 01 // 屬性結構
- attributes_info attributes[attributes_count]
- }
方法屬性結構
- attribute_info {
- u2 attribute_name_index; 00 09 // Code
- u4 attribute_length; 00 00 00 1D 長度 29
- u1 info[attribute_length];
- ...
- }
7. 屬性表集合
00 09 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 03 00
"Code" 表示下面是執行代碼
JVM 預定義了一部分的attribute, 但是編譯器自己也可以實現自己的attribute 寫入class文件中, 供運行時使用。不同的attribute 通過attribute_name_index 來區分。
JVM 規范預定義的attribute
Code 結構
Code attribute 的作用是保存該放的的結構,如所對應的字節碼
- Code_attribute {
- u2 attribute_name_index; // 00 09 ==> Code
- u4 attribute_length; // 00 00 00 1D ==> 29
- u2 max_stack; // 00 01 棧深度為 1 (棧幀中操作數棧的深度)
- u4 code_length; // 00 00 00 05 指令的長度是多少
- u1 code[code_lenght]; // 2A B7 00 01 B1 其中 2A
- // 2A aload_0
- // B7 invokespecial
- // 00 #1
- // 01 <java/lang/Object.<init>>
- // B1 return
- // 0 aload_0
- // 1 invokespecial #1 <java/lang/Object.<init>>
- // 4 return
- u2 exception_table_length; // 00 00
- {
- u2 start_pc;
- u2 end_pc;
- u2 handler_pc;
- u2 catch_type;
- } exception_table[exception_table_lenght];
- u2 attributes_count; // 00 01
- attribute_info attributes[attributes_count];
- }
attribute_length 表示 attribute 所包含的字節數, 不包含attribute_name_index 和 attribute_length 字段。
max_stack 表示這個方法運行的任何時刻所能達到的操作數棧的最大深度。
max_locals 表示方法執行期間創建的局部變量的數目,包含用來表示傳入的參數的局部變量。
code_length 表示該方法所包含的字節碼的字節數以及具體的指令碼。
具體字節碼即是該方法被調用時,虛擬機所執行的字節碼。
exception_table, 這里存放的是處理異常信息。
每個 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 的結構
- LineNumberTable_attribute {
- u2 attribute_name_index; //00 0A 常量池10號位置 LineNumberTable
- u4 attribute_lenght; // 00 00 00 06 一共 6個長度
- u2 line_number_table_length; // 00 01 映射的對數 1 對
- line_number_info {
- u2 start_pc; // 指令行號 00 00
- u2 line_number; // 源碼調試 00 03
- }
- line_number_table[line_number_table_length];
- }
局部變量表 LocalVariableTable
- LocalVariableTable_attribute {
- u2 attribute_name_index;
- u4 attribute_lenght;
- u2 local_variable_table_length;
- {
- u2 start_pc;
- u2 line_number;
- u2 name_index;
- u2 descriptor_index;
- u2 index;
- }
- }
總結
- 構造方法中會初始化成員屬性的默認值,如果自己實現了默認的構造方法, 依然還是在構造方法中賦值,這就是對指令的重排序。
- 如果多個構造方法那么每個構造方法中都有初始化成員變量的屬性,來保障每個構造方法初始化的時候都會執行到屬性的初始化過程。
- 如果構造方法中有執行語句, 那么會先執行賦值信息, 然后在執行自定義的執行代碼。
- 對于Java每一個實例方法(非靜態方法), 其在編譯后生成的字節碼中比實際方法多一個參數, 它位于方法的第一個參數位置. 我們就可以在當前方法中 的this去訪問當前對象中的this這個操作是在Javac 編譯器在編譯期間將this的訪問轉換為對普通實例方法的參數訪問,接下來在運行期間, 由JVM的調用實例方法時, 自動向實例方法中傳入該this參數, 所以在實例方法的局部變量表中, 至少會一個指向當前對象的局部變量。
- 字節碼對于處理異常的方式:
- 統一采用異常表的方式來對異常處理。
- 在Jdk1.4.2之前的版本中, 并不是使用異常表的方式來對異常進行處理的,而是采用特定的指令方式。
- 當異常處理存在finally 語句塊時,現代化的JVM才去的處理方式將finally語句塊的自己拼接到每一個catch塊后面, 換句話說,程序中中存在多個catch塊,就會在每一個catch塊后面重復多少個finally 的語句塊字節碼。