1. 程式人生 > 其它 >jvm學習筆記:棧幀

jvm學習筆記:棧幀

棧幀內的資料結構

  • 區域性變量表(Local Variables):記錄非靜態方法的this指標、方法引數、區域性變數

  • 運算元棧(Operand Stack):用於計算的棧結構

  • 動態連結(Dynamic Link):指向執行時常量池的方法引用

  • 方法返回地址(Return Address):方法正常退出或異常退出的定義,以及方法間返回值傳遞(注意: java中的方法除了正常返回還可能因為異常退出,所以方法返回地址不一定就是方法退出的地址)

  • 附加資訊

區域性變量表

Each frame contains an array of variables known as its local variables

. The length of the local variable array of a frame is determined at compile-time and supplied in the binary representation of a class or interface along with the code for the method associated with the frame

主要儲存方法的引數、方法內的區域性變數,資料型別包括基本資料型別、物件引用(數值地址)、返回地址。區域性變量表具有以下特點:

  • 區域性變量表陣列的容量大小在編譯時期確定,後面不會變化
  • 區域性變量表是每個執行緒私有的,不存線上程安全問題

非靜態方法的區域性變量表

  • 構造方法、例項方法會自帶一個this引用變數
  • static方法不能使用this的原因:靜態方法的區域性變量表中不帶有this的引用

變數槽(Slot)

區域性變量表中最基本的儲存單元,每個槽大小32位

  • 區域性變量表是index0開始的陣列,陣列的每個元素我們稱為變數槽
  • 區域性變量表中,32位以內型別佔一個槽(int),64位佔兩個槽(long、double),引用型別佔用一個槽
  • 呼叫佔有兩個槽的變數使用起始索引

slot重複利用

  • 棧幀中區域性變量表的槽位是可以複用的,如果某個區域性變數的作用域已經結束了,那麼在它後面宣告的區域性變數可以使用它的槽位

  • 這樣的設計使區域性變量表的空間利用率更高,不會造成空位的情況。

    重複利用的例子:

    public void test1(){
    	int a = 0;
    	{
    		int b = 0;
    		b = a + 1;
    	}
    	int c = 1;
    }
    

上面程式碼的區域性變量表如圖,可見b和c的槽位都是index=2,也就是說b的作用域結束後,c會重複使用b的槽位。

區域性變量表與垃圾回收

  • 區域性變量表中的變數是重要的垃圾回收節點,只有被區域性變量表中直接或者間接引用的物件才不會被回收

運算元棧(Operand Stack)

Each frame contains a last-in-first-out (LIFO) stack known as its operand stack. The maximum depth of the operand stack of a frame is determined at compile-time and is supplied along with the code for the method associated with the frame

運算元棧,在方法執行的過程中根據位元組碼指令,往棧中寫入或提取資料

  • 某些位元組碼指令將值壓入運算元棧,其他指令又可以取出數值,使用後又可以再壓入棧
  • 比如:複製操作、交換、求和
  • 位元組碼指令由執行引擎翻譯成機器指令
  • 運算元棧是一個棧幀中的結構,它隨著一個方法的開始執行而被建立
  • 每一個運算元棧都在編譯時確定了固定的深度,這個深度儲存在code屬性的max_stack值
  • 32位佔一個棧深度、64位佔兩個棧深度

i++和++i區別

情景1

public void test(){
       int i1 = 10;
       i1++;

       int i2 = 20;
       ++i2;
   }		

上面程式碼編譯的位元組碼如上圖,可見i++和++i編譯出來的位元組碼其實是一樣的,首先都是bipush指令將i的初值壓入運算元棧,然後 用istore指令出棧並存入區域性變量表。然後用iinc指令將區域性變量表中的資料取出並+1

情景2

public void test(){
       int i1 = 10;
       int d1 = i1++;

       int i2 = 20;
       int d2 = ++i2;
   }

上面程式碼編譯後位元組碼如圖,可以發現用i++賦值和++i賦值的位元組碼是有差異的。

int d = i++的情況:首先還是用 bipush指令把初始值放入運算元棧,然後用istore取出並存入區域性變量表。在給d賦值時,會先用 iload指令讀取區域性變量表的 i 到運算元棧(暫時存在棧中,不出棧),然後用 iinc指令為區域性變量表的 i 加1,完成後再用istore指令將運算元棧中沒有加一的 i 出棧,存到新的區域性變量表槽中。

int d = ++i的情況:與上述情況唯一的不同是,這種情況會先呼叫 iinc 指令將區域性變量表的 i 加一,然後再用 iload指令將加一後的 i存入運算元棧,最後用 istore賦值給區域性變數

情景3(喪心病狂)

public void test(){
       int i1 = 10;

       i1 = i1++;

       int i2 = 10;
       i2 = ++i2;
   }

上述程式碼的位元組碼如上圖

i = i ++情況:首先使用了 bipush 將10 壓入了運算元棧,然後使用了 istore將初值10出棧並存入區域性變量表,然後用 iload指令將區域性變數 10 入棧到了運算元棧(接下來的操作該數值沒改變)。接著使用了 iinc 指令將區域性變數 i 取出、入棧、加一、出棧、存入區域性變量表。這一過程沒有改變最初入棧的初值10。最後,使用了 istore 指令將這個 10 覆蓋了之前加一的區域性變數。所以這種情況其實 i 的值沒變。

i = ++i情況:同樣,還是先用bipush、istore將初值10存入了局部變量表。因為++再i之前,所以先使用了 iinc 指令將區域性變量表中的 10+1 改成了 11,然後再用 iload、istore指令來給 i 賦值,最終i會變成 11

其實通過上面兩種情況的分析,我們可以發現,第二種情況下最後的iload、istore指令其實是沒有實際意義的,它們只是將 i 入棧運算元棧又出棧存入原來的區域性變量表位置。

動態連結(Dynamic Linking)

Each frame contains a reference to the run-time constant pool for the type of the current method to support dynamic linking of the method code

每個棧幀中包含的指向執行時常量池中該棧幀所屬方法的引用

  • 指向執行時常量池的方法引用
  • 動態連結的作用就是將符號引用轉換為直接引用