一、運行時棧幀結構
棧幀是虛擬機進行方法調用與執行的數據結構,屬於線程私有的虛擬機棧中的棧元素。每個方法從調用到完成,對應一個棧幀在虛擬機棧中入棧到出棧的過程。
1. 局部變量表
- 存放方法參數與方法內定義的局部變量
- 以 變量槽 為最小單位,可容納
boolean/byte/char/short/int/float/reference/returnAddress 類型
- 長整型和雙精度佔 2 個槽
- 通過索引訪問,索引範圍
[0, maxLocals)
- 方法調用時,參數按順序傳遞至局部變量表(實例方法第 0 槽為
this 引用)
2. 操作數棧
- 後進先出(LIFO)棧,也稱操作棧
- 方法執行初始為空,字節碼指令對其進行入棧/出棧操作
- 如
iadd:彈出兩個 int 相加,結果入棧
- 最大深度在編譯期確定,寫入 Code 屬性的
max_stack
3. 動態連接
- 每個棧幀持有指向運行時常量池中該方法引用的指標
- 支持方法調用過程中的動態連接:將符號引用轉為直接引用
- 區別於靜態解析(類加載階段完成),動態連接發生在運行期
4. 方法返回地址
- 正常完成出口:遇到返回字節碼指令(
ireturn、areturn 等),可能有返回值傳遞給調用者
- 異常完成出口:方法拋出異常且未在方法體內捕獲,通過異常表查找處理器,若無匹配則向上拋出
二、方法調用
方法調用不等同於方法執行,調用階段唯一任務是確定被調用方法的版本(即調用哪個方法),不涉及具體執行邏輯。
1. 解析調用
- 靜態過程,編譯期/類加載解析階段即可確定
- 適用於
static、private、final、構造器、父類方法(super.xxx())
- 符號引用直接轉為直接引用,不會延遲到運行期
2. 分派調用
分派調用可能是靜態的,也可能是動態的,根據分派依據分為:
- 靜態分派:依據靜態類型(聲明類型),編譯期確定。典型場景:方法重載
- 動態分派:依據實際類型,運行期確定。典型場景:方法重寫,核心是
invokevirtual 指令的多態查找過程
3. 虛擬機如何實現動態分派
- 在類的方法表中建立索引,子類繼承父類方法表,重寫方法覆蓋同索引位置
invokevirtual 執行時:獲取對象實際類型 → 查找該類型方法表 → 取同索引方法 → 執行
- 這就是「單分派」(根據對象實際類型分派)
補充:棧幀與性能優化
- 棧幀大小 影響方法調用深度,遞歸過深導致
StackOverflowError
- 逃逸分析 可實現標量替換、棧上分配、同步消除,減少堆分配壓力
- 方法內聯 將短小熱點方法內聯到調用者,消除調用開銷,是 JIT 編譯器最重要的優化手段