JVM 位元組碼執行例項分析
最近在看《Java 虛擬機器規範》和《深入理解JVM虛擬機器》,對於位元組碼的執行有了進一步的瞭解。位元組碼就像是組合語言,是 JVM 的指令集。下面我們先對 JVM 執行引擎做一下簡單介紹,然後根據例項分析 JVM 位元組碼的執行過程。
執行時棧幀結構
棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區中的虛擬機器棧的棧元素。棧幀儲存了方法的區域性變量表,運算元棧,動態連線和方法返回地址等資訊。每一個方法從呼叫開始至執行完成的過程,都對應著一個棧幀在虛擬機器棧裡面從入棧到出棧的過程。
在編譯程式設計師程式碼的時候,棧幀中區域性變量表和運算元棧的大小已經確定了,並且寫入到方法表中的 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 塊中的位元組碼實現的。