三言兩語:JVM 字節(jié)碼執(zhí)行實(shí)例分析

前言

最近在看《Java 虛擬機(jī)規(guī)范》和《深入理解JVM虛擬機(jī)》,對于字節(jié)碼的執(zhí)行有了進(jìn)一步的了解。字節(jié)碼就像是匯編語言,是 JVM 的指令集。下面我們先對 JVM 執(zhí)行引擎做一下簡單介紹,然后根據(jù)實(shí)例分析 JVM 字節(jié)碼的執(zhí)行過程。包括:

  1. for 循環(huán)字節(jié)碼分析
  2. 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
  1. 字節(jié)碼中 0 ~ 4 行將整數(shù) 1 賦值為變量 x,x 存儲(chǔ)在 slot0 中,并且將 x 的值拷貝一份放到 slot1。如果沒有出現(xiàn)異常,繼續(xù)走到 5 ~ 7 行,將 x 賦值為 3,然后讀取 slot1 中的值到棧頂,最后ireturn返回棧頂?shù)闹?,方法結(jié)束。
  2. 如果出現(xiàn)異常,PC 寄存器指針轉(zhuǎn)到第 8 行,第 8 ~ 16 行所做的事情就是將 2 賦值給 x,然后保存 x 的拷貝,最后將 x 賦值為 3。方法返回前將 x 的拷貝 2 讀取到棧頂。
  3. 如果在 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ī)》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容