【Java虛擬機器】棧幀和方法呼叫
棧幀和方法呼叫
執行時棧幀結構
棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區中的虛擬機器棧的棧元素。
棧幀儲存了方法的區域性變量表、運算元棧、動態連線和方法放回地址等資訊。
每個方法從呼叫考試至執行完成的過程,都對應著一個棧幀在虛擬機器棧裡面從入棧到出棧的過程。
棧幀的概念結構如下圖所示:
區域性變量表
區域性變量表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。
區域性變量表以變數槽(slog)為最小單位,每個slot都應該能存放一個boolean、byte、char、short、int、float、reference或returnAddress型別的資料,long和double資料型別佔用兩個slot
區域性變量表中的slot是可以重用的
區域性變數不像類變數那樣存在“準備階段”,也就是沒有預設值。
運算元棧
32位資料型別所佔的棧容量為1,64位資料型別所佔的棧容量為2,。
當一個方法剛剛開始執行的時候,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令往運算元棧中寫入\提取內容,也就是出棧\入棧操作。
動態連線
每個棧幀都包含一個指向執行時常量池中改棧幀所述方法的引用,持有這個引用是為了支援方法呼叫過程在的動態連線。
一些符號引用會在類載入階段就轉化為直接引用,這種轉化成為靜態解析。另一部分將在每一次執行期間轉化為直接引用,這部分成為動態連線。
返回地址
遇到返回指令,把返回值傳遞給上層的方法呼叫者
方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,導致方法退出,沒有返回值返回給呼叫者
方法呼叫
解析
在類載入的解析階段,會將其中一部分符號引用轉化為直接引用,這種解析能成立的前提是:方法在程式真正執行之前就有一個可確定的呼叫版本,並且這個方法的呼叫版本在執行期間是不可變的。這類方法的呼叫成為解析。
只要被invokestatic和invokespecial指令呼叫的方法,都可以在解析階段中確定唯一的呼叫版本,符合這個條件的有靜態方法、私有方法、例項構造器、父類方法4類
還有被final修飾的方法,也是可以確定的
分派
解析呼叫一定是一個靜態的過程,分派呼叫可能是靜態的也可能是動態的。
靜態分派
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello, guy!");
}
public void sayHello(Man guy) {
System.out.println("hello, getleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello, lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
}
輸出結果為:
hello, guy!
hello, guy!
Human man = new Man();
“Human”成為變數的靜態型別,後面的“Man”則成為變數的實際型別
實際型別變化
Human man = new Man();
man = new Woman();
靜態型別變化
staticDispatch.sayHello((Man) man);
staticDispatch.sayHello((Woman) man);
過載時是通過引數的靜態型別而不是實際型別作為判斷的,並且靜態型別是編譯期可知的。
動態分配
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
輸出結果為:
man say hello
woman say hello
重寫是根據實際型別來呼叫的。
解析過程如下
- 找到實際型別,記作C
- 如果在型別C中找到與常量中的描述符和簡單名稱都相符的方法,則返回此引用
- 否則,按照繼承關係從下往上依次對C的各個父類進行第二步的搜尋
- 否則,失敗
虛擬機器動態分配的實現
動態分配是非常頻繁的動作,基於效能的考慮,大多數會在方法區中建立一個虛方法表,來代替元資料查詢以提高效能。
一個例子的虛方法表結構如下圖所示:
虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類沒有被重寫,那子類的虛方法表裡面的地址入口和父類相同的方法的地址入口是一致的。如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實現版本的入口地址。
方法表一般在類載入的連線階段進行初始化,準備了類的變數初始值後,虛擬機器會把類的方法表也初始化完畢。
參考
- 深入理解Java虛擬機器[書籍]