JVM 棧幀之操作數棧與局部變數表

  • 2019 年 10 月 3 日
  • 筆記

前置知識

閱讀本文需要對以下知識有所了解:
* 棧
* 彙編
* Java 基礎
* 逆波蘭表達式(有學過的同學閱讀本文毫無障礙)

引子

基於暫存器的設計模式

就我們所熟知的x86或arm指令集來說,其對數據的操作都是基於暫存器。例如,要對兩個數執行加法操作則需要將這兩個數分別送入兩個暫存器再執行加法操作,這也符合我們對於程式語言認知,更加易於理解。

基於棧的設計模式

基於棧的設計模式則是將數據存放在棧中,在需要使用的時候將棧頂的數據出棧,並執行相應的操作。

舉例來說,在JVM中 執行 a = b + c 的位元組碼執行過程中操作數棧以及局部變數表的變化如下圖所示。

局部變數表中存儲著a、b、c 三個局部變數,首先將b和c分別入棧
圖1

將棧頂的兩個數出棧執行加法操作,並將結果保存至棧頂,之後將棧頂的數出棧賦值給a
圖2

一個簡單的例子

在上一節中我們了解了棧與局部變數表是如何配合完成一次加法操作的,這一節我們將對局部變數表進行深入的研究。

如何查看局部變數表?

我們可以通過反編譯class文件的方式查看局部變數表,不過在這裡更加推薦使用IDEA的jclasslib插件(直接搜就有)查看位元組碼,因為其設計更加人性化,更加友好。

實例方法中的局部變數表

我們知道在實例方法中我們可以直接訪問實例的成員變數或函數,而不需要通過this來引用,這是如何實現的?
像這種問題直接動手寫個測試類,反編譯一下結果自然就清晰了。
首先來一個簡單的類

public class T {        private int a = 0;        public void add(int b,int c){          a = b + c;      }  }

其次,將該類選中,然後在view中選中showbytecode with jclasslib
圖3

效果如下:
圖4

選擇我們關注的Methods中add方法,注意觀察圖中高亮的部分
圖5

原來,JVM在編譯程式碼的時候,偷偷在局部變數表中添加了一個this引用(很明顯this保存的實例的引用),這也是我們為什麼可以在方法中訪問實例中的成員變數的原因,證明如下
圖6

圖中節碼的簡要解釋如下:
0)aload_0 將this的引用入棧 (aload_0即將局部變數表中索引為0的引用壓到操作數棧中)
1)iload_1 將參數b入棧 (將局部變數中的索引為1的整數壓到操作數棧中)
2)iload_2 將參數c入棧

此時棧的內容有(0為棧頂)
0.c
1.b
2.this

3)iadd 將棧頂的兩個數相加,並將結果保存至棧頂,此時棧的內容為
0.b+c
1.this

4). putfield 將棧頂的兩個值出棧,第一個值(b+c)賦值給第二個值(this)的對應的成員變數(是的,沒錯即使是賦值也要執行兩次出棧操作)
putfield的說明如下(注意圖中的高亮部分):
putfield
地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.putfield

結論

基於棧的指令集系統可以很方便的做到平台無關性(x86、arm),但也降低了性能,這也是為啥Java性能比C低原因。(操作暫存器快,還是操作棧快?哈哈哈哈哈哈哈哈哈哈哈)