前言
最近在看《Java 虛擬機(jī)規(guī)范》和《深入理解JVM虛擬機(jī)》,對于字節(jié)碼的執(zhí)行有了進(jìn)一步的了解。字節(jié)碼就像是匯編語言,是 JVM 的指令集。下面我們先對 JVM 執(zhí)行引擎做一下簡單介紹,然后根據(jù)實(shí)例分析 JVM 字節(jié)碼的執(zhí)行過程。包括:
- for 循環(huán)字節(jié)碼分析
- try-catch-finally 字節(jié)碼分析
運(yùn)行時(shí)棧幀結(jié)構(gòu)
棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧的棧元素。棧幀存儲(chǔ)了方法的局部變量表,操作數(shù)棧,動(dòng)態(tài)連接和方法返回地址等信息。每一個(gè)方法從調(diào)用開始至執(zhí)行完成的過程,都對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過程。
在編譯程序員代碼的時(shí)候,棧幀中局部變量表和操作數(shù)棧的大小已經(jīng)確定了,并且寫入到方法表中的 Code 屬性中。
在活動(dòng)線程中,只有位于棧頂?shù)臈攀怯行У模?稱為當(dāng)前棧幀,與這個(gè)棧幀關(guān)聯(lián)的方法稱為當(dāng)前方法。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令只對當(dāng)前棧幀進(jìn)行操作。
局部變量表
局部變量表是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。局部變量表的容量以變量槽(slot)為最小單位,每個(gè) slot 保證能放下 32 位內(nèi)的數(shù)據(jù)類型。虛擬機(jī)通過索引定位的方式使用局部變量表,索引值從 0 開始。值得注意的是,對于實(shí)例方法,局部變量表中第 0 位索引的 slot 默認(rèn)是 this引用;靜態(tài)方法則不是。而且為了節(jié)約內(nèi)存,slot 是可以重用的。
操作數(shù)棧
操作數(shù)棧的元素可以是任意的 Java 數(shù)據(jù)類型。當(dāng)一個(gè)方法開始時(shí),這個(gè)方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會(huì)有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是出棧入棧操作。
實(shí)例分析
下面分析的字節(jié)碼指令主要是對局部變量表和操作棧的讀寫。
for 循環(huán)字節(jié)碼分析
void spin() {
int i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}
}
上面是一個(gè)空循環(huán)的代碼,編譯后的字節(jié)碼如下:
Method void spin()
0 iconst_0 // Push int constant 0
1 istore_1 // Store into local variable 1 (i=0)
2 goto 8 // First time through don’t increment
5 iinc 1 1 // Increment local variable 1 by 1 (i++)
8 iload_1 // Push local variable 1 (i)
9 bipush 100 // Push int constant 100
11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done
相信大家看到上面的代碼都是一臉懵逼,即使有注釋還是不知道字節(jié)碼到底做了什么操作。下面我就圖解每一條指令,幫助理解。上面的代碼都是對局部變量表和操作數(shù)棧的操作,所以我們的關(guān)注點(diǎn)就在這兩個(gè)區(qū)域上。(棧是自頂向下的)
0 iconst_0 //把常量0放入棧
+--------+--------+
| local | stack |
+-----------------+
| | 0 |
+-----------------+
| | |
+--------+--------+
1 istore_1 //把棧頂?shù)脑爻鰲#娴骄植孔兞勘硭饕秊?的位置
+--------+--------+
| local | stack |
+-----------------+
| 0 | |
+-----------------+
| | |
+--------+--------+
2 goto 8 //跳轉(zhuǎn)到第8條指令
8 iload_1 //把局部變量表中索引為1的變量入棧
+--------+--------+
| local | stack |
+-----------------+
| 0 | 0 |
+-----------------+
| | |
+--------+--------+
9 bipush 100 //把100入棧
+--------+--------+
| local | stack |
+-----------------+
| 0 | 0 |
+-----------------+
| | 100 |
+--------+--------+
11 if_icmplt 5 //出棧兩個(gè)元素v1,v2,比較它們的值,當(dāng)且僅當(dāng)v1 < v2,跳轉(zhuǎn)到指令5
+--------+--------+
| local | stack |
+-----------------+
| 0 | |
+-----------------+
| | |
+--------+--------+
5 iinc 1 1 //自增局部變量表中索引為1的值
+--------+--------+
| local | stack |
+-----------------+
| 1 | |
+-----------------+
| | |
+--------+--------+
//進(jìn)行下次循環(huán)直到指令11不滿足,到達(dá)指令14
14 return //清空棧,執(zhí)行引擎把控制權(quán)交換給調(diào)用者。
+--------+--------+
| local | stack |
+-----------------+
| 100 | |
+-----------------+
| | |
+--------+--------+
以上就是for循環(huán)字節(jié)碼執(zhí)行的過程??梢园l(fā)現(xiàn),所有指令都是圍繞者局部變量表和操作數(shù)棧在操作。
解惑
指令iconst_0,iload_1的命名解讀
第一個(gè)i代表這是對int數(shù)據(jù)類型進(jìn)行的操作
const,load是操作碼
0,1是隱含的操作數(shù)
上面的兩個(gè)指令等價(jià)于iconst 0,iload 1
詳細(xì)的字節(jié)碼解釋查閱《JVM 虛擬機(jī)規(guī)范》
try-catch-finally 字節(jié)碼分析
static int inc(){
int x;
try {
x = 1;
return x;
} catch (Exception e){
x = 2;
return x;
} finally {
x = 3;
}
}
下面是它的字節(jié)碼,這次我就不畫圖了,里面的命令跟上面的類似。
static int inc();
descriptor: ()I
flags: ACC_STATIC
Code:
stack=1, locals=4, args_size=0
0: iconst_1 //try 塊中的 x = 1;
1: istore_0 //保存棧頂元素到局部變量表中索引為 0 的 slot 中
2: iload_0 //加載局部變量表中索引為 0 的值到棧中
3: istore_1 //保存棧頂元素到局部變量表中索引為 1 的 slot 中
4: iconst_3 //finally 塊中的 x = 3;
5: istore_0 //保存棧頂元素到局部變量表中索引為 0 的 slot 中,x 的值存在這里。
6: iload_1 //加載局部變量表中索引為 1 的值到棧中
7: ireturn //返回棧頂元素,即 x = 1;正常情況下函數(shù)運(yùn)行到這里就結(jié)束了,如果出現(xiàn)異常根據(jù)異常表跳轉(zhuǎn)到指定的位置
8: astore_1 //給 catch 塊中定義的 Exception e 賦值,存儲(chǔ)在 slot1 中。
9: iconst_2 //catch 塊中的 x = 2;
10: istore_0
11: iload_0
12: istore_2
13: iconst_3 //finally 塊中的 x = 3;
14: istore_0
15: iload_2
16: ireturn //此時(shí)返回的是 slot2 中的值,即 x = 2
17: astore_3 //如果出現(xiàn)不屬于 java.lang.Exception 及其子類的異常,才會(huì)根據(jù)異常表中的規(guī)則跳轉(zhuǎn)到這里。
18: iconst_3 //finally 塊中的 x = 3;
19: istore_0
20: aload_3 //將異常加載到棧頂,
21: athrow //拋出棧頂?shù)漠惓? Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
- 字節(jié)碼中 0 ~ 4 行將整數(shù) 1 賦值為變量 x,x 存儲(chǔ)在 slot0 中,并且將 x 的值拷貝一份放到 slot1。如果沒有出現(xiàn)異常,繼續(xù)走到 5 ~ 7 行,將 x 賦值為 3,然后讀取 slot1 中的值到棧頂,最后
ireturn返回棧頂?shù)闹?,方法結(jié)束。 - 如果出現(xiàn)異常,PC 寄存器指針轉(zhuǎn)到第 8 行,第 8 ~ 16 行所做的事情就是將 2 賦值給 x,然后保存 x 的拷貝,最后將 x 賦值為 3。方法返回前將 x 的拷貝 2 讀取到棧頂。
- 如果在 0 ~ 4,8 ~ 13 行中出現(xiàn)其他異常,則跳轉(zhuǎn)到第 17 行執(zhí)行,先同樣執(zhí)行
finally塊中的x = 3,最后拋出異常,方法結(jié)束。
可以看到,Java 的異常處理是通過異常表的方式來決定代碼執(zhí)行的路徑。而finally的實(shí)現(xiàn)是通過在每個(gè)路徑的最后加入finally塊中的字節(jié)碼實(shí)現(xiàn)的。
參考資料
《Java 虛擬機(jī)規(guī)范》
《深入理解JVM虛擬機(jī)》