JVM 位元組碼執行例項分析

Yonah-瀟發表於2016-05-19

前言

最近在看《Java 虛擬機器規範》和《深入理解JVM虛擬機器》,對於位元組碼的執行有了進一步的瞭解。位元組碼就像是組合語言,是 JVM 的指令集。下面我們先對 JVM 執行引擎做一下簡單介紹,然後根據例項分析 JVM 位元組碼的執行過程。包括:

  1. for 迴圈位元組碼分析
  2. try-catch-finally 位元組碼分析

執行時棧幀結構

棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區中的虛擬機器棧的棧元素。棧幀儲存了方法的區域性變數表,運算元棧,動態連線和方法返回地址等資訊。每一個方法從呼叫開始至執行完成的過程,都對應著一個棧幀在虛擬機器棧裡面從入棧到出棧的過程。

在編譯程式設計師程式碼的時候,棧幀中區域性變數表和運算元棧的大小已經確定了,並且寫入到方法表中的 Code 屬性中。

在活動執行緒中,只有位於棧頂的棧幀才是有效的, 稱為當前棧幀,與這個棧幀關聯的方法稱為當前方法。執行引擎執行的所有位元組碼指令只對當前棧幀進行操作。

區域性變數表

區域性變數表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。區域性變數表的容量以變數槽(slot)為最小單位,每個 slot 保證能放下 32 位內的資料型別。虛擬機器通過索引定位的方式使用區域性變數表,索引值從 0 開始。值得注意的是,對於例項方法,區域性變數表中第 0 位索引的 slot 預設是 this引用;靜態方法則不是。而且為了節約記憶體,slot 是可以重用的。

運算元棧

運算元棧的元素可以是任意的 Java 資料型別。當一個方法開始時,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令往運算元棧中寫入和提取內容,也就是出棧入棧操作。

例項分析

下面分析的位元組碼指令主要是對區域性變數表和操作棧的讀寫。

for 迴圈位元組碼分析

void spin() {
    int i;
    for (i = 0; i < 100; i++) {
        ; // Loop body is empty
    }
}

上面是一個空迴圈的程式碼,編譯後的位元組碼如下:

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

相信大家看到上面的程式碼都是一臉懵逼,即使有註釋還是不知道位元組碼到底做了什麼操作。下面我就圖解每一條指令,幫助理解。上面的程式碼都是對區域性變數表和運算元棧的操作,所以我們的關注點就在這兩個區域上。(棧是自頂向下的)

0 iconst_0 //把常量0放入棧
+--------+--------+
| local  | stack  |
+-----------------+
|        |   0    |
+-----------------+
|        |        |
+--------+--------+

1 istore_1 //把棧頂的元素出棧,存到區域性變數表索引為1的位置
+--------+--------+
| local  | stack  |
+-----------------+
|   0    |        |
+-----------------+
|        |        |
+--------+--------+

2 goto 8 //跳轉到第8條指令

8 iload_1 //把區域性變數表中索引為1的變數入棧
+--------+--------+
| local  | stack  |
+-----------------+
|   0    |   0    |
+-----------------+
|        |        |
+--------+--------+

9 bipush 100 //把100入棧
+--------+--------+
| local  | stack  |
+-----------------+
|   0    |   0    |
+-----------------+
|        |  100   |
+--------+--------+

11 if_icmplt 5 //出棧兩個元素v1,v2,比較它們的值,當且僅當v1 < v2,跳轉到指令5
+--------+--------+
| local  | stack  |
+-----------------+
|   0    |        |
+-----------------+
|        |        |
+--------+--------+

5 iinc 1 1 //自增區域性變數表中索引為1的值
+--------+--------+
| local  | stack  |
+-----------------+
|   1    |        |
+-----------------+
|        |        |
+--------+--------+

//進行下次迴圈直到指令11不滿足,到達指令14
14 return //清空棧,執行引擎把控制權交換給呼叫者。
+--------+--------+
| local  | stack  |
+-----------------+
|   100  |        |
+-----------------+
|        |        |
+--------+--------+

以上就是for迴圈位元組碼執行的過程。可以發現,所有指令都是圍繞者區域性變數表和運算元棧在操作。

解惑
指令iconst_0,iload_1的命名解讀
第一個i代表這是對int資料型別進行的操作
const,load是操作碼
0,1是隱含的運算元
上面的兩個指令等價於iconst 0,iload 1
詳細的位元組碼解釋查閱《JVM 虛擬機器規範》

try-catch-finally 位元組碼分析

static int inc(){
    int x;
    try {
        x = 1;
        return x;
    } catch (Exception e){
        x = 2;
        return x;
    } finally {
        x = 3;
    }
}

下面是它的位元組碼,這次我就不畫圖了,裡面的命令跟上面的類似。

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;正常情況下函式執行到這裡就結束了,如果出現異常根據異常表跳轉到指定的位置
     8: astore_1 //給 catch 塊中定義的 Exception e 賦值,儲存在 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 //此時返回的是 slot2 中的值,即 x = 2
    17: astore_3 //如果出現不屬於 java.lang.Exception 及其子類的異常,才會根據異常表中的規則跳轉到這裡。
    18: iconst_3 //finally 塊中的 x = 3;
    19: istore_0
    20: aload_3 //將異常載入到棧頂,
    21: athrow //丟擲棧頂的異常
  Exception table:
     from    to  target type
         0     4     8   Class java/lang/Exception
         0     4    17   any
         8    13    17   any

位元組碼中 0 ~ 4 行將整數 1 賦值為變數 x,x 儲存在 slot0 中,並且將 x 的值拷貝一份放到 slot1。如果沒有出現異常,繼續走到 5 ~ 7 行,將 x 賦值為 3,然後讀取 slot1 中的值到棧頂,最後ireturn返回棧頂的值,方法結束。

如果出現異常,PC 暫存器指標轉到第 8 行,第 8 ~ 16 行所做的事情就是將 2 賦值給 x,然後儲存 x 的拷貝,最後將 x 賦值為 3。方法返回前將 x 的拷貝 2 讀取到棧頂。

如果在 0 ~ 4,8 ~ 13 行中出現其他異常,則跳轉到第 17 行執行,先同樣執行finally塊中的x = 3,最後丟擲異常,方法結束。

可以看到,Java 的異常處理是通過異常表的方式來決定程式碼執行的路徑。而finally的實現是通過在每個路徑的最後加入finally塊中的位元組碼實現的。

參考資料

《Java 虛擬機器規範》、《深入理解JVM虛擬機器》

相關文章