Java 執行時棧桢

6個月前 (11-27) Yosheng 程式設計 0評論 已收錄 133℃

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

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

概述

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

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

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

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

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


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

局部變量表

我們在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編譯期輸出的指令流,是基於棧的指令集合架構。

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

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


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


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

並且將100推入操作數棧。


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



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



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

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


博主

擅長使用 C# 和 Java 開發項目,全棧開發工程師,前端主要使用 Vue 其次 Angular ,目前正在學習分布式架構,運維研發兼具,平時愛好鑽研技術並應用於實務當中,常駐於上海。

相關推薦

相逢就是有緣,留下足跡吧!