Java虛擬機器之位元組碼執行引擎
1 概述
虛擬機器執行引擎是Java虛擬機器最核心的部分之一,其目的是實現:輸入位元組碼檔案,將位元組碼解析或等效處理後,執行並輸出結果。
其中兩種執行方式:解釋執行和編譯執行。
2 執行時棧幀結構
棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,他是虛擬機器執行時資料區中的虛擬機器棧(Virtual Machine Stack)的棧元素。
棧幀儲存了方法的區域性變量表、運算元棧、動態連線、方法返回地址和一些額外的附件資訊等資訊。編譯程式碼的時候,棧幀需要的區域性變量表大小,運算元棧的深度等都已經儲存在Code屬性中。棧幀大小不受執行期變數資料的影響。每一個方法從呼叫開始至執行完成過程,都是對應著一個棧幀在虛擬機器棧裡面從入棧到出棧的過程。
對於執行引擎來說,在活動執行緒中,只有位於棧頂的棧幀才是有效的。稱為當前棧幀(Current Stack Frame).與這個棧幀相關聯的方法稱為當前方法(Current Method).執行引擎的所有位元組碼指令都是隻針對當前棧幀進行操作的,典型的棧幀結構如下:
2.1 區域性變量表
區域性變量表是變數值的儲存空間,儲存的是方法引數和方法內部定義的區域性變數,其容量用Slot1作為最小單位。在編譯生成的class檔案中,在方法的Code屬性的max_locals
資料項中確定了該方法所需要分配的區域性變量表的最大容量。虛擬機器是通過索引定位的方式使用區域性變量表的,範圍是 [0-max_locals
由於區域性變量表是建立線上程的棧上,是執行緒的私有資料,因此不存在資料安全問題。
在方法執行時,虛擬機器通過使用區域性變量表完成引數值到引數變數列表的傳遞過程。如果是例項方法(非static方法),那區域性變量表第0位索引的Slot儲存的是方法所屬物件例項的引用,因此在方法內可以通過關鍵字this來訪問到這個隱含的引數。其餘的引數按照引數表順序排列,引數表分配完畢之後,再根據方法體內定義的變數的順序和作用域分配。
和類變數的兩次初始化不同的是,區域性變量表不存在系統初始化的過程,這意味著一旦定義了局部變數則必須人為的初始化,否則無法使用。
2.2 運算元棧
運算元棧(Operand Stack)是一個LIFO棧,運算元棧最大深度在編譯成class檔案的時候也已經在Code屬性中max_stacks
當一個方法剛開始執行的時候,其運算元棧為空。方法執行過程中各種位元組碼指令往運算元棧寫入和讀取內容,對應著入棧和出棧操作。
2.3 動態連線
每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,以支援動態連線。
2.4 方法返回地址
一個方法執行時,有且只有兩張方式可以使此方法返回:執行引擎遇到任意一個方法返回的位元組碼指令,這時可能會由返回值傳遞給上層的方法呼叫者,此時屬於正常退出;另一種是方法執行的時候遇到異常,且此異常未在方法體中捕獲處理,將導致方法退出。
方法退出的時候,都需要返回到方法被呼叫的位置,程式才能繼續執行,方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。方法正常退出時,呼叫者的PC計數器的值可以作為返回地址,棧幀中可能儲存這個計數器值;方法異常退出時,返回地址是通過異常處理表確定的,棧幀中一般不會儲存這部分資訊。
方法退出過程相當於當前棧幀出棧,退出操作可能需要執行:恢復上層方法的區域性變量表和運算元棧,如果有返回值則把返回值壓入呼叫者棧幀的運算元棧中;調整PC計數器執行方法呼叫的指令後面一條指令等。
3 方法呼叫
方法呼叫並不等同於方法的執行,方法呼叫的唯一任務是:確定被呼叫方法的版本(即呼叫哪個方法)。由於Java編譯成class檔案時,只是儲存了方法的符號引用,而沒有直接引用(即方法的記憶體地址),故這裡涉及到以下步驟。
3.1 解析
所有方法呼叫的目標方法都是在class檔案中一個常量池的符號引用,故在類載入的解析階段會把一部分符號引用轉化為直接引用。此解析成立的前提是:方法在程式真正執行之前就有一個可以確定的呼叫版本,且此方法的呼叫版本在執行期是不可改變的。符合此前提的方法的呼叫稱為解析(Resolution),也就是說解析式一個靜態過程。
Java中,符合解析的方法主要包括靜態方法和私有方法兩類,前者是於類直接關聯的;後者在外部不可訪問。由於其特點決定其無法被重寫或覆蓋,故適合在類載入時載入解析。其實final修飾的方法,也是符合此約定的,會再類載入時解析。但由於其是使用invokevirtual呼叫的,故這裡單獨給出。
Java提供了五種呼叫方法的位元組碼指令:
invokestatic(呼叫靜態方法);invokespecial(呼叫例項構造器,私有方法和父類方法);invokevirtual(呼叫所有的虛方法);invokeinterface(呼叫介面方法,會再執行時確定一個介面的實現);invokedynamic(先在執行時動態的解析出呼叫點限定符所引用的方法再執行該方法)
invokestatic和invokespecial指令呼叫的方法,都可以在解析階段唯一確定呼叫版本,也就是會再類載入時會被解析。
3.2 分派
分派是實現Java多型性的基礎。分為靜態、動態;單分派、多分派。
注意:
過載:引數靜態型別;
重寫:引數動態型別
3.2.1 靜態分派
所有依賴靜態型別來定位方法執行版本的分派稱為靜態分派。靜態分派的典型應用是方法過載。
這裡有一點需要注意:編譯器在 過載時 是通過引數的靜態型別而不是實際型別作為判斷依據的,且靜態型別在編譯時已知,因此在編譯階段,javac會根據引數的靜態型別決定使用哪個版本的過載。
例如以下示例,最終打印出來的會試兩個 Hello, Human!
:
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, Human!");
}
public void sayHello(Man guy) {
System.out.println("Hello, Man!");
}
public void sayHello(Woman guy) {
System.out.println("Hello, Woman!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
}
方法過載引數的匹配規則:char->int->long->float->double->裝箱類->裝箱類的介面型別->裝箱類從下往上的父型別->可變引數型別,也就是說首先會進行自動轉型,然後是裝箱操作,接著是裝箱後的介面(實現了多個介面則優先順序一致),再然後父類(從下往上遞迴),最後才是可變引數。
3.2.2動態分派
動態分派是在虛擬機器執行的時候的分派操作。涉及到invokevirtual指令,這個指令的多型查詢規則如下:
- 找到運算元棧頂的第一個元素所指物件的實際型別 C .
- 若型別C中找到與常量中的描述符和簡單名稱都匹配的方法,則進行訪問許可權校驗,通過則返回此方法的直接引用,查詢結束;未通過則返回java.lang.IllegalAccessError
- 若沒找到匹配的方法則按照繼承關係從下往上依次對C的父類進行第2部的搜尋和驗證過程。
- 若最終沒有找到合適的方法,則丟擲java.lang.AbstractMethodError
3.2.3 單分派與多分派
靜態分派的時候根據靜態型別來決定方法的引數;然後動態分派在執行的時候,根據動態分派來獲取方法的接受者的實際型別,再執行其實際型別來達到動態分派。
3.2.4 虛方法表/介面方法表
虛方法表vtable是類在方法區建立的一個表,表中存放了各個方法的實際入口地址,以避免在類的方法元資料中頻繁查詢,優化查詢速度。
虛方法表中除了子類的方法外,還包含父類的方法。若某個方法在子類沒有被重寫,則子類的虛方法表裡面該方法的入口地址和父類中相同方法的入口地址是一致的,都指向父類的實現入口;若子類重寫了方法,則子類虛方法表中該方法的地址會替換成子類實現版本的入口地址。
父子虛方法表中,相同簽名的方法具有相同的索引序號,這樣在型別變換時,只需要變更查詢的虛方法表就能獲取到實際需要的入口地址。
虛介面表itable與上述類似,不再詳述。
3.3動態語言支援
JDK1.7在虛擬機器指令集中增加了invokedynamic
;於此相應,增加了java.lang.invoke
包,通過MethodHandle,提供了一種有別於之前( 單純依靠符號引用來確定呼叫目標方法機制),通過動態方式確定目標方法的機制,以此添加了對動態語言的支援。
java.lang.invoke
包含了一些類和方法以實現動態語言支援,主要有以下這些
- MethodHandle:此物件可以代表一個可以呼叫方法的’引用’
- MethodType:代表方法型別:MethodType.methodType()第一個引數為返回值,後續引數為方法具體引數。
- MethodHandles.lookup():在指定方法中查詢符合給定的方法名稱方法型別並且符合呼叫許可權的方法控制代碼。
與反射的區別如下:
- Reflection是在模擬java程式碼層次的方法呼叫;MethodHandle是模擬位元組碼層次的方法呼叫。
- Reflection是重量級的,包含了方法的幾乎java端所有資訊,比如方法簽名,描述符,方法屬性表各種屬性,執行許可權等等;而MethodHandle是輕量級的,僅僅包含了執行方法的相關資訊。