一篇文章快速搞懂Java虛擬機的棧幀結構

什麼是棧幀?

正如大家所瞭解的,Java虛擬機的內存區域被劃分爲程序計數器、虛擬機棧、本地方法棧、堆和方法區。(什麼?你還不知道,趕緊去看看《Java虛擬機內存結構及編碼實戰》)這次要介紹的棧幀(Stack Frame),就是Java虛擬機中的虛擬機棧(Virtual Machine Stack)的基本元素,它也是用於支持Java虛擬機進行方法調用和方法執行背後的數據結構,瞭解了它就可以更好地理解Java虛擬機執行引擎是如何運行的。

每一個方法從調用開始至執行結束的整個過程,都對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息,在同一時刻、同一條線程中,只有位於棧頂的方法纔是在運行的,只有位於棧頂的棧幀纔是生效的,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。虛擬機棧和棧幀的總體結構如下圖:

接下來,再分別介紹一下棧幀中的局部變量表、操作數棧、動態連接、方法返回地址等各個部分的作用和數據結構。

局部變量表(Local Variables Table)

局部變量表是用來存儲一組變量值的內存空間,用於存放方法參數和方法內部定義的局部變量。在已經編譯好的Class文件中,方法的Code屬性的max_locals數據項中,就確定了該方法所需分配的局部變量表的最大容量。

局部變量表的容量以變量槽(Variable Slot)爲最小單位,每個變量槽存放一個32位數據類型,如boolean、byte、char、short、int、float和reference這幾種類型。前6種類型同學們應該都瞭解,就不必多介紹了,reference類型表示對一個對象實例的引用,通過這個引用做到兩件事情:根據引用直接或間接地查找到實例在Java堆中的數據存放的起始地或索引;根據引用直接或間接地查找到在方法區中的存儲的類信息。對於64位數據類型,如long和double這兩種類型,是以高位對齊的方式爲其分配兩個連續的變量槽空間。

使用局部變量表時,通過索引定位對應數據的位置,索引值的範圍是從0開始至局部變量表最大的變量槽數量。如果訪問的是32位數據類型的變量,索引N就代表了使用第N個變量槽,如果訪問的是64位數據類型的變量,則說明會同時使用第N和N+1兩個變量槽。對於兩個相鄰的共同存放一個64位數據的兩個變量槽,虛擬機不允許採用任何方式單獨訪問其中的某一個,如果遇到進行這種操作的字節碼,Java虛擬機就會在類加載的校驗階段中拋出異常。

當一個方法被調用時,會使用局部變量表來完成參數值到參數變量列表的傳遞過程。如果執行的是對象實例的成員方法(沒有被static修飾的方法),那麼局部變量表中第0位索引的變量槽默認就是該對象實例的引用,在方法中可以通過關鍵字this來訪問到這個隱含的參數。其餘參數則按照參數表順序排列,參數表分配完畢後,再根據方法體內部定義的局部變量順序和作用域分配其餘的變量槽。爲了儘可能節省棧幀所耗的內存空間,局部變量表中的變量槽是可以重用的,當方法體中定義的局部變量超出其作用域時,該局部變量對應的變量槽就可以交給其他變量來重用。

之前的《JVM的類加載機制全面解析》中介紹過,在類加載過程中,類變量有兩次賦初始值的過程,一次在準備階段,賦予系統初始值;另外一次在初始化階段,賦予代碼中定義的初始值。因此即使沒有爲類變量賦值也沒有關係,類變量仍然具有一個確定的初始值,不會產生歧義。但是局部變量不像類變量有那樣的“準備階段”,如果一個局部變量定義了但沒有賦初始值,那它是完全不能使用的。所以不要認爲Java中任何情況下都存在諸如整型變量默認爲0、布爾型變量默認爲false等這樣的默認值規則。比如:

public class OneMoreStudy {
    public static void main(String[] args) {
        int i;
        System.out.println(i);
    }
}

因爲局部變量i沒有初始,在編譯過程就會報錯:

Error:(4, 28) java: 可能尚未初始化變量i

操作數棧(Operand Stack)

操作數棧是一個後入先出(Last In First Out,LIFO)棧。和局部變量表一樣,在已經編譯好的Class文件中,方法的Code屬性的max_stacks數據項中,就確定了該方法所需分配的操作數棧的最大深度。在方法執行的任何時候,操作數棧的深度都不會超過在max_stacks數據項中設定的最大值。操作數棧的每一個元素都可以是包括long和double在內的任意Java數據類型。32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。

當一個方法剛剛開始執行的時候,該方法的操作數棧是空的,在該方法的執行過程中,會有各種字節碼指令對操作數棧進行出棧和入棧的操作。比如,整數加法的字節碼指令iadd,在該指令執行前必須保證操作數棧中最接近棧頂的兩個元素已經存入了兩個int型的數值,當該指令執行時,會把這兩個int值出棧並相加,然後將相加的結果重新入棧。

操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯代碼時,編譯器會嚴格保證這一點,在類加載的校驗階段也會再次驗證這一點。在上面的iadd指令中,只能用於整型數的加法,它在執行時,最接近棧頂的兩個元素的數據類型必須爲int型,不能出現其他數據類型使用iadd命令相加的情況。

一個方法調用另外一個方法時,可以通過操作數棧來進行方法參數的傳遞。雖然在Java虛擬機規範中,兩個不同棧幀作爲不同方法的虛擬機棧的元素,是完全相互獨立的。但是在大多Java虛擬機的實現是,都會進行一些優化:兩個不同方法的棧幀出現一部分重疊。讓下面棧幀的部分操作數棧與上面棧幀的部分局部變量表重疊在一起,這樣做不僅節約了一些內存空間,更重要的是在進行方法調用時就可以直接共用一部分數據,不需要進行額外的參數複製和傳遞,如下圖:

動態連接(Dynamic Linking)

每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。

之前的《Class文件結構全面解析》中介紹過,Class文件的常量池中存有大量的符號引用,這些符號引用一部分會在類加載階段或者第一次使用的時候就被轉化爲直接引用(實際運行時內存佈局中的入口地址),這種轉化被稱爲靜態解析。另外一部分將在每一次運行期間都轉化爲直接引用,這部分就稱爲動態連接。關於這兩個轉化過程的具體過程,這裏先賣個關子,後續的文章會詳細介紹。

方法返回地址

方法返回時可能需要在棧幀中保存一些信息,用來於恢復調用者(調用當前方法的方法)的執行狀態。一般來說,方法正常退出時,調用者的程序計數器的值就可以作爲返回地址,棧幀中很可能會保存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中就一般不會保存這部分信息。

方法返回的過程實際上等同於把當前棧幀出棧,可能執行的操作有:恢復調用者的局部變量表和操作數棧,把返回值(如果有的話)壓入調用者棧幀的操作數棧中,調整程序計數器的值使其指向方法調用指令後面的一條指令等等。

附加信息

在Java虛擬機規範中,允許Java虛擬機增加一些規範裏沒有描述的信息到棧幀之中,比如:調試、性能收集相關的信息,這部分信息完全取決於具體的虛擬機實現。一般會把動態連接、方法返回地址和其他附加信息全部歸爲一類,稱爲棧幀信息。

總結

棧幀是Java虛擬機中的虛擬機棧的基本元素,每一個方法從調用開始至執行結束的整個過程,都對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址和其他附加信息。局部變量表用於存放方法參數和方法內部定義的局部變量;各種字節碼指令執行時,會對操作數棧進行出棧和入棧的操作;動態連接是指向運行時常量池中該棧幀所屬方法的引用;方法返回地址用於恢復調用當前方法的方法的執行狀態。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章