三言兩語: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 塊中的字節碼實現的。
參考資料
《Java 虛擬機規范》
《深入理解JVM虛擬機》