基於棧的執行引擎
字節碼是運行在 JVM 上的,爲了能弄懂字節碼,需要對 JVM 的運行原理有所瞭解。這篇文章將以棧幀爲切入點理解字節碼在 JVM 上執行的細節。
虛擬機
兩者有什麼不同呢?舉一個計算兩數相加的例子:c = a + b 基於 HotSpot JVM 的源碼和字節碼如下
源碼
void bar(int a, int b) {
int c = a + b;
}
對應字節碼
0: iload_1 // 將 a 壓入操作數棧
1: iload_2 // 將 b 壓入操作數棧
2: iadd // 將棧頂兩個值出棧,相加,然後將結果放回棧頂
3: istore_3 // 將棧頂值存入局部變量表中第 3 個 slot 中
基於寄存器的 LuaVM 的 lua 源碼和字節碼如下,查看字節碼使用luac -l -l -v -s test.lua
命令
源碼
local function my_add(a, b)
return a + b;
end
對應字節碼
1 [3] ADD 2 0 1
基於寄存器的 add 指令直接把寄存器 R0 和 R1 相加,結果保存在寄存器 R2 中。
基於棧和基於寄存器的過程對比如下:
爲了理解字節碼的細節,我們需要詳細瞭解字節碼的執行過程。衆所周知,Hotspot JVM 是一個基於棧的虛擬機,每個線程都有一個虛擬機棧,存儲了「棧幀」。每次方法調用都伴隨着棧幀的創建銷燬。
棧幀
棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構 棧幀隨着方法調用而創建,隨着方法結束而銷燬,棧幀的存儲空間分配在 Java 虛擬機棧中,每個棧幀擁有自己的局部變量表(Local Variables)、操作數棧(Operand Stack) 和 指向運行時常量池的引用
局部變量表
每個棧幀內部都包含一組稱爲局部變量表(Local Variables)的變量列表,局部變量表的大小在編譯期間就已經確定。Java 虛擬機使用局部變量表來完成方法調用時的參數傳遞,當一個方法被調用時,它的參數會被傳遞到從 0 開始的連續局部變量列表位置上。當一個實例方法(非靜態方法)被調用時,第 0 個局部變量是調用這個實例方法的對象的引用(也就是我們所說的 this )
操作數棧
每個棧幀內部都包含了一個稱爲操作數棧的後進先出(LIFO)棧,棧的大小同樣也是在編譯期間確定。Java 虛擬機提供的一些字節碼指令用來從局部變量表或者對象實例的字段中複製常量或者變量到操作數棧,也有一些指令用於從操作數棧取走數據、操作數據和把操作結果重新入棧。在方法調用時,操作數棧也用來準備調用方法的參數和接收方法返回的結果。
比如 iadd 指令用來將兩個 int 類型的數值相加,它要求執行之前操作數棧已經存在兩個由前面其它指令放入的 int 型數值,在 iadd 指令執行時,兩個 int 值從操作數棧中出棧,相加求和,然後將求和的結果重新入棧。
比如 1 + 2
這樣的指令執行過程如下
public class ScoreCalculator {
public void record(double score) {
}
public double getAverage() {
return 0;
}
}
public static void main(String[] args) {
ScoreCalculator calculator = new ScoreCalculator();
int score1 = 1;
int score2 = 2;
calculator.record(score1);
calculator.record(score2);
double avg = calculator.getAverage();
}
javap 查看字節碼輸出如下
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=6, args_size=1
0: new #2 // class ScoreCalculator
3: dup
4: invokespecial #3 // Method ScoreCalculator."<init>":()V
7: astore_1
8: iconst_1
9: istore_2
10: iconst_2
11: istore_3
12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method ScoreCalculator.record:(D)V
18: aload_1
19: iload_3
20: i2d
21: invokevirtual #4 // Method ScoreCalculator.record:(D)V
24: aload_1
25: invokevirtual #5 // Method ScoreCalculator.getAverage:()D
28: dstore 4
30: return
12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method ScoreCalculator.record:(D)V
可以看到 aload_1 先從局部變量表中 1 的位置加載 calculator 對象,iload_2 從 局部變量表中 2 的位置加載一個整型值,i2d 這個指令用來將整型值轉爲 double 並將新的值重新入棧,到目前爲止參數全部就緒,可以用 invokevirtual 執行方法調用了
- 24 ~ 28:同樣是一個普通的方法調用,流程還是先 aload_1 加載 calculator 對象,invokevirtual 調用 getAverage 方法,並將 棧頂元素存儲到局部變量表下標爲 4 的位置上 有一點需要注意的是 javap 輸出的 locals=6,但是我們目前看到的局部變量只有
args、calculator、score1、score2、avg
這 5 個,爲什麼這裏等於 6 呢?這是因爲 avg 爲 double 型變量,需要兩個槽位(slot) 整個過程局部變量表如下圖所示
vap -v -p -l MyLocalVariableTest
並沒有輸出任何局部變量表相關的信息。這是因爲默認情況下局部變量表屬於調試級別的信息,javac 編譯的時候並沒有編譯進字節碼,我們可以加上 javac -g 生成字節碼的時候同時生成所有的調試信息,如下所示
javac -g MyLocalVariableTest.java
javap -v -p -l MyLocalVariableTest
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 calculator LScoreCalculator;
10 21 2 score1 I
12 19 3 score2 I
30 1 4 avg D
從二進制看 class 文件和字節碼
public class Get {
String name;
public String getName() {
return name;
}
}
javap 查看字節碼如下
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
小結
一起來回顧一下這篇文章的要點:
-
第一,基於棧和基於寄存器指令集的優劣勢;
-
第二,講解了 JVM 棧幀的構成(局部變量表、操作數棧、指向運行時常量池的引用),順帶講解了 javap -l 參數和其在局部變量表中的應用;
-
第三,從類文件二進制角度看字節碼的實現,並引出 ASM 字節碼改寫技術。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/tOZ7t8Tm1ZLZets3KnNeRw