棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區的虛擬機器棧(Virtual Machine Stack)的棧元素。棧幀儲存了方法的區域性變數表,運算元棧,動態連線和方法返回地址等資訊。第一個方法從呼叫開始到執行完成,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。
每一個棧幀都包括了區域性變數表
,運算元棧
,動態連線
,方法返回地址
和一些額外的附加資訊
。在編譯程式碼的時候,棧幀中需要多大的區域性變數表,多深的運算元棧都已經完全確定了,並且寫入到了方法表的Code屬性中,因此一個棧幀需要分配多少記憶體,不會受到程式執行期變數資料的影響,而僅僅取決於具體虛擬機器的實現。
一個執行緒中的方法呼叫鏈可能會很長,很多方法都同時處理執行狀態。對於執行引擎來講,活動執行緒中,只有虛擬機器棧頂的棧幀才是有效的,稱為當前棧幀(Current Stack Frame),這個棧幀所關聯的方法稱為當前方法(Current Method)。執行引用所執行的所有位元組碼指令都只針對當前棧幀進行操作。棧幀的概念結構如下圖所示:
1. 區域性變數表
區域性變數表(Local Variable Table)是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。在編譯Class檔案時,就在方法的Code屬性的max_locals資料項中已經確定了該方法需要分配的區域性變數表的最大容量。
變數槽 (Variable Slot)
是區域性變數表的最小單位,沒有強制規定大小為 32 位,雖然32位足夠存放大部分型別的資料。一個 Slot
可以存放 boolean
、byte
、char
、short
、int
、float
、reference
和 returnAddress
8種型別。其中 reference
表示對一個物件例項的引用,通過它可以得到物件在Java 堆中存放的起始地址的索引和該資料所屬資料型別在方法區的型別資訊。returnAddress
則指向了一條位元組碼指令的地址。 對於64位的 long 和 double 變數而言,虛擬機器會為其分配兩個連續的 Slot 空間。
虛擬機器通過索引定位的方式使用區域性變數表。之前我們知道,**區域性變數表存放的是方法引數和區域性變數。當呼叫方法是非static 方法時,區域性變數表中第0位索引的 Slot 預設是用於傳遞方法所屬物件例項的引用,即 “this” 關鍵字指向的物件。**分配完方法引數後,便會依次分配方法內部定義的區域性變數。
Slot複用驗證
為了節省棧幀空間,區域性變數表中的 Slot 是可以重用的。當離開了某些變數的作用域之後,這些變數對應的 Slot 就可以交給其他變數使用。這種機制有時候會影響垃圾回收行為。
public class Main {
public static void main(String[] args) {
byte[] placeholder = new byte[64*1024*1024];
System.gc();
}
}
複製程式碼
[GC (System.gc()) 69468K->66384K(188416K), 0.0016481 secs]
[Full GC (System.gc()) 66384K->66280K(188416K), 0.0079337 secs]
複製程式碼
public class Main {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64*1024*1024];
}
int a = 0;
System.gc();
}
}
複製程式碼
[GC (System.gc()) 69468K->66368K(188416K), 0.0012876 secs]
[Full GC (System.gc()) 66368K->744K(188416K), 0.0055897 secs]
複製程式碼
可以看到,當我吧byte的宣告單獨放到程式碼塊中,然後再執行作用域之外的程式碼的時候,gc對slot進行了回收。
注意:jvm不會給區域性變數賦初始值,只給全域性變數賦初始值。
2. 運算元棧
運算元棧(Operand Stack)也常稱為操作棧,是一個後入先出棧。在Class 檔案的Code 屬性的 max_stacks 指定了執行過程中最大的棧深度。Java 虛擬機器的解釋執行引擎稱為”基於棧的執行引擎“,這裡的棧就是指運算元棧。
方法執行中進行算術運算或者是呼叫其他的方法進行引數傳遞的時候是通過運算元棧進行的。
jvm對運算元棧的優化
在概念模型中,兩個棧幀是相互獨立的。但是大多數虛擬機器的實現都會進行優化,令兩個棧幀出現一部分重疊。令下面的部分運算元棧與上面的區域性變數表重疊在一塊,這樣在方法呼叫的時候可以共用一部分資料,無需進行額外的引數複製傳遞。
3. 動態連結
每個棧幀都包含一個執行執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線(Dynamic Linking)。
Class 檔案中存放了大量的符號引用,位元組碼中的方法呼叫指令就是以常量池中指向方法的符號引用作為引數。這些符號引用一部分會在類載入階段或第一次使用時轉化為直接引用
,這種轉化稱為靜態解析。另一部分將在每一次執行期間轉化為直接引用
,這部分稱為動態連線。
4. 方法返回地址
當一個方法開始執行以後,只有兩種方法可以退出當前方法:
- 當執行遇到返回指令,會將返回值傳遞給上層的方法呼叫者,這種退出的方式稱為正常完成出口(Normal Method Invocation Completion),一般來說,
呼叫者的PC計數器可以作為返回地址
。 - 當執行遇到異常,並且當前方法體內沒有得到處理,就會導致方法退出,此時是沒有返回值的,稱為異常完成出口(Abrupt Method Invocation Completion),
返回地址要通過異常處理器表來確定
。
當方法返回時,可能進行3個操作:
恢復
上層方法的區域性變數表和運算元棧- 把返回值
壓入
呼叫者呼叫者棧幀的運算元棧 調整
PC 計數器的值以指向方法呼叫指令後面的一條指令
5. 附加資訊
虛擬機器規範允許具體的虛擬機器實現增加一些規範裡沒有描述的資訊到棧幀之中,例如與除錯相關的資訊,這部分資訊完全取決於具體的虛擬機器實現。在實際開發中,一般會把動態連線、方法返回地址與其他附加資訊全部歸為一類,稱為棧幀資訊。