Java 執行時棧桢

1,252次閱讀
尚無留言

共计 2911 个字符,预计需要花费 8 分钟才能阅读完成。

本篇文章從簡體轉為台灣繁體並轉換對應的用語,原文可參考 Java —— 运行时栈帧结构

程式碼編譯的結果從本地機器碼轉變為位元組碼,是儲存格式發展的一小步,卻是程式語言發展的一大步。

概述

棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構。它是虛擬機器執行時資料區中的虛擬機器棧的棧元素。

棧幀儲存了方法的局部變量表、操作數棧、動態連線和方法返回地址等資訊。

每一個方法從呼叫開始至執行完成的過程,都對應著一個棧幀在虛擬機器裡面從入棧到出棧的過程。

在編譯程式程式碼的時候,棧幀中需要多大的局部變量表,多深的操作數棧都已經完全確定了。

因此一個棧幀需要分配多少記憶體,不會受到程式執行期變數資料的影響,而僅僅取決於具體的虛擬機器實現。

Java 執行時棧桢

在活動執行緒中,只有位於棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法。

局部變量表

我們在 Java 記憶體區域中在虛擬機器棧提到:對於我們來說,主要關注的 stack 棧記憶體,就是虛擬機器棧中局部變量表部分。

也就是說局部變量表在棧中起著舉足輕重的作用。

局部變量表(Local Variable Table)是一組變數值儲存空間,用於存放方法參數和方法內部定義的局部變數。並且在 Java 編譯為 Class 檔案時,就已經確定了該方法所需要分配的局部變量表的最大容量

變數槽(Variable Slot)

局部變量表的容量以變數槽為最小單位,每個變數槽都可以儲存 32 位長度的記憶體空間,例如 boolean、byte、char、short、int、float、reference。

對於 64 位長度的資料類型(long,double),虛擬機器會以高位對齊方式為其分配兩個連續的 Slot 空間,也就是相當於把一次 long 和 double 資料類型讀寫分割成為兩次 32 位讀寫。

reference(物件例項的引用)

一般來說,虛擬機器都能從引用中直接或者間接的查詢到物件的以下兩點:
①在 Java 堆 中的資料存放的起始地址索引。
②所屬資料類型在 方法區 中的儲存的類型資料。

整形 字节(b) bit(位)
byte 1 1*8
short 2 2*8
int 4 4*8
long 8 8*8

 

浮点型 字节(b) bit(位)
folat 4 4*8
double 8 8*8

Char 类型 字节(b) bit(位)
char 2 2*8

 

boolean 类型 字节(b) bit(位)
boolean 1 1*8

PS : 8bit=1b,1024b=1kb

在方法執行時,虛擬機器使用局部變量表完成參數值到參數變數列表的傳遞過程的,如果執行的是類方法,那局部變量表中第 0 位索引的 Slot 預設是用於傳遞方法所屬物件類的引用。(在方法中可以通過關鍵字 this 來訪問到這個隱含的參數)。

其餘參數則按照參數表順序排列,佔用從 1 開始的局部變數 Slot。

Slot 複用

為了盡可能節省棧幀空間,局部變量表中的 Slot 是可以重用的,也就是說當 PC 計數器的指令指已經超出了某個變數的作用域(執行完畢),那這個變數對應的 Slot 就可以交給其他變數使用。
優點:節省棧幀空間
缺點: 影響到系統的垃圾收集行為。(如大方法佔用較多的 Slot,執行完該方法的作用域後沒有對 Slot 賦值或者清空設定 null 值,垃圾回收器便不能及時的回收該記憶體。)

動態連接

每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支付方法呼叫過程中的動態連接(Dynamic Linking)。

在類載入階段中的解析階段會將符號引用轉為直接引用,這種轉化也稱為靜態解析。另外的一部分將在每一次執行時期轉化為直接引用。這部分稱為動態連接。(關於這部分,後面會再繼續分析)

方法返回地址

當一個方法開始執行後,只有 2 種方式可以退出這個方法:

  • 方法返回指令:執行引擎遇到一個方法返回的位元組碼指令,這時候有可能會有返回值傳遞給上層的方法呼叫者,這種退出方式稱為正常完成出口。
  • 異常退出:在方法執行過程中遇到了異常,並且沒有處理這個異常,就會導致方法退出。

無論採用任何退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才能繼續執行,方法返回時可能需要在棧幀中儲存一些資訊。

一般來說,方法正常退出時,呼叫者的 PC 計數器的值可以作為返回地址,棧幀中會儲存這個計數器值。

而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會儲存這部分資訊。

操作數棧

操作數棧和局部變量表一樣,在編譯時期就已經確定了該方法所需要分配的局部變量表的最大容量。

操作數棧的每一個元素可用是任意的 Java 資料類型,包括 long 和 double。32 位資料類型所佔的棧容量為 1,64 位資料類型佔用的棧容量為 2。

當一個方法剛剛開始執行的時候,這個方法的操作數棧是空的,在方法執行的過程中,會有各種位元組碼指令往操作數棧中寫入和提取內容,也就是 出棧 / 入棧操作

例如,在做 算術運算 的時候是通過操作數棧來進行的,又或者在呼叫其它方法的時候是通過操作數棧來進行 參數傳遞 的。

參數傳遞

Java 執行時棧桢

索然兩個棧幀作為虛擬機器棧的元素是完全獨立的,但是虛擬機器會做出相應的優化,令兩個棧幀出現一部分重疊。

如上圖所示,棧幀的部分操作數棧與上一個棧幀的局部變量表重疊在一起,這樣在進行方法呼叫時就可以共用一部分資料,無須進行額外的參數複製傳遞。

算術運算

Java 編譯期輸出的指令流,是基於棧的指令集合架構。

例如我們來看一段簡單的算術程式碼:

public class Calc {public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return  (a + b) * c;
    }
}

通過 javap 檢視位元組 碼指令:

Java 執行時棧桢

首先這段程式碼需要深度為 2 的操作數棧和 4 個 Slot 的局部變數空間。下面引用《深入理解 Java 虚拟机 II》的圖片來描述程式碼執行的結果,操作數棧和局部變量表的變化情況。

Java 執行時棧桢

首先局部變量表中第 0 位索引的 Slot 預設是用於傳遞方法所屬物件例項的引用

並且將 100 推入操作數棧。

Java 執行時棧桢

然後將操作數棧的值出棧,並存儲在局部變量表中。

Java 執行時棧桢

Java 執行時棧桢

將要運算的數值複製到操作數棧中(入棧)。

Java 執行時棧桢

Java 執行時棧桢

操作數棧運算(100+200)後出棧,再將結果(300)入棧。(因為還要繼續運算)。

最後需要 乘運算,將數值 300 入棧。

Java 執行時棧桢

正文完
 0
評論(尚無留言)