JVM規范之棧幀
- 前言
- 正文
- 概述
- 局部變量表
- 操作數棧
- 動態鏈接
- 總結
- 參考鏈接
前言
上一篇文章了解了JVM規范中的運行時數據區:
JVM規范之運行時數據區域
其中,棧是JVM線程私有的內存區,棧中存儲的單位是幀(frames),本篇文章通過JVM8規范學習棧幀在JVM運行時的作用。
正文
概述
每個棧幀的內存分配自線程私有的 Java 虛擬機棧(JVM Stack),線程調用方法時壓棧,方法結束時出棧銷毀。
幀的作用是保存方法調用的局部變量、中間結果、執行動態鏈接、方法返回信息、發送異常。一個幀通常包含局部變量表、操作數棧、運行時常量池引用,這些組件都是幀私有的。
創建/銷毀時機:
當方法被調用的時候,棧幀也會被創建,當方法調用完成時,棧幀被銷毀,方法調用的含義包含兩種:
- 方法正常調用結束,方法運行期間,JVM 沒有拋出或者沒有顯式地使用throw拋出未被捕獲的異常;
- 方法非正常結束(如拋出未捕獲異常)時,棧幀會提前銷毀,JVM 通過異常棧軌跡(StackTrace)記錄各層棧幀信息,用于調試;
內存分配策略:
局部變量表和操作數棧的大小在編譯時確定,運行時常量池引用的大小本質是一個指針,具體大小可能取決于具體的 JVM 實現,因此棧幀的大小由編譯時確定的局部變量表和操作數棧的理論最大值,結合 JVM 實現的內存布局(如 Slot 字節數、對齊策略)決定。
局部變量表
- 每個棧幀都有自己的局部變量表,局部變量表的大小是在編譯時確定的,因此在運行時可以一次性從棧上進行分配;
- 局部變量表通過索引的方式訪問,每個索引的位置可以理解一個變量槽(slot),每個槽可以存儲
boolean
,byte
,char
,short
,int
,float
,reference
,returnAddress
類型的值,一對slot可以存儲long
和double
類型的值; long
和double
占用兩個連續的變量槽,比如一個long
占用了索引n
和n+1
兩個變量槽的位置,但是n+1
位置是不可讀取的,可以被寫入,這會導致變量槽n
位置的數據失效;- JVM 規范沒有限制局部變量表中的數據必須進行字節對齊;
- 在調用方法時,JVM 使用局部變量表傳遞參數,從索引位置
0
開始,如果調用的方法是一個實例方法,索引0
位置總是被傳入this
;對于靜態方法,局部變量表索引0
不存儲this
,直接從索引0
開始存儲方法參數。
操作數棧
- 每個棧幀都包含一個操作數棧,棧的最大深度是編譯時確定的,當棧幀被創建時,操作數棧是空的;
- 操作數棧符合棧的特點,LIFO,操作數棧中每個entry可以容納一個JVM數據類型,包含
long
和double
類型; - 操作數棧中存儲的數據類型和操作指令必須嚴格匹配,JVM會在進行class文件驗證時,檢查操作數棧的使用是否符合規范;
- 操作數棧中,
long
/double
作為 64 位值,占用 2 個深度單位,其他類型占 1 個;
動態鏈接
什么是動態鏈接?
在 JVM 中,動態鏈接是類加載機制和運行時環境的關鍵環節,主要用于將符號引用(Symbolic References)解析為直接引用(Direct References),是實現多態的核心技術。
- 每個棧幀都包含一個運行時常量池的引用,用于實現方法調用的動態鏈接過程;
- 字節碼文件中描述了方法調用的符號表引用,但是并沒有實際代碼的內存地址,因此動態鏈接是將符號表引用翻譯為直接引用;
- 動態鏈接是實現多態的核心機制,通過運行時常量池解析虛方法的符號引用,在運行時根據對象實際類型(如instanceof)找到具體方法的直接引用。例如,子類重寫父類方法時,編譯期符號引用指向父類,運行時動態鏈接到子類實現。
補充:
- 在編譯階段,字節碼文件中僅保存符號表引用,不會涉及靜態鏈接或者動態鏈接的過程;
- 靜態鏈接在類加載階段(解析階段)確定方法調用目標,將符號表引用轉化為直接引用,而動態鏈接指JVM在運行時才能確定調用目標的實例類型,在運行時將符號表引用轉化為直接引用。
動態鏈接代碼示例:
public class Animal {public void sound() {System.out.println("Animal"); } // 虛方法
}
public class Dog extends Animal {@Overridepublic void sound() {System.out.println("Woof");} // 動態鏈接目標
}
// 調用時通過運行時常量池解析為Dog.sound()
Animal animal = new Dog();
animal.sound(); // invokevirtual指令觸發動態鏈接
總結
本篇文章根據JVM 8規范了解了棧幀的數據結構:
- 棧幀是在JVM線程進行方法調用時創建,每調用一個方法就會創建一個棧幀,每當方法調用結束或者異常結束,棧幀會被出棧;
- 每個棧幀通常包含局部變量表、操作數棧、運行時常量池引用,棧幀的內存占用大小僅取決于JVM的實現,其中,局部變量表類似一個數組,JVM線程通過索引的方式讀取局部變量表的內容;操作數棧用于保存中間結果和方法保存結果,要求操作數棧中的數據類型和指令必須匹配;運行時常量池引用是JVM實現動態鏈接的關鍵,當然也用來支持類字段引用和其它常量的訪問;
JVM 規范僅定義棧幀的邏輯結構(如局部變量表、操作數棧),具體實現(如 Slot 是否對齊、指針壓縮)由廠商決定。例如,HotSpot 中 Slot 通常為 32 位,long/double占 2 個 Slot,無需 64 位對齊。
參考鏈接
jvm8s