1、概述
Java虛擬機器規範制定了虛擬機器位元組碼執行引擎的概念模型,本章主要從概念模型層次來探究虛擬機器的方法呼叫和位元組碼執行。
方法呼叫中,最核心的,是如何確定呼叫的方法,也就是方法的分派。
位元組碼執行過程中,特別重要的一點是執行上下文的切換和資訊的交換處理。這需要執行時資料結構的支援,也就是執行時棧幀。
2、執行時棧幀結構
執行時棧幀(Stack Frame)是用於支援虛擬機器方法呼叫和方法執行的資料結構。
它是虛擬機器執行時資料區中的虛擬機器棧的棧元素。
儲存了方法的區域性變數表、運算元棧、動態連結和方法返回地址等資訊。
方法的呼叫、執行、返回過程就是棧幀在棧裡入棧(建立)、內部資訊改變、出棧(銷燬)的過程。
在編譯過程中,棧幀中的區域性變數表的大小、運算元棧的深度就已經確定並記錄在了方法的code屬性裡面了。
對於執行引擎來說,只有棧頂的棧幀(當前棧幀,對應當前方法)是有效的。
1、區域性變數表
存放方法引數和方法內部定義的區域性變數。
容量以槽(Slot)為最小單位。
虛擬機器規範沒有規定槽的大小,
只說了每個槽都能存放一個boolean、byte、char、short、int、float、reference、或 returnAddress資料型別。
因此可以說一個Slot可以存放一個32位及以下的資料型別。
64位的資料型別要佔用兩個Slot(long、double),高位對齊。
reference資料至少要能幫助虛擬機器完成兩項功能:
1、直接或間接地查詢到物件在Java堆中的起始地址;
2、直接或間接地在方法區中查詢到物件所屬資料型別(物件的後設資料)。
區域性變數列表中,索引從0開始,第0位存放的是方法隱含的引數this(非static方法)。
其餘位置先按引數列表的順序存放引數,再按區域性變數定義的順序存放區域性變數。
區域性變數表中的引用會影響到GC的行為,因為它是GC Roots之一。
如果區域性變數表中的引用還存在,那麼GC就不會清除引用指向的物件。
將物件引用置為null來幫助GC的原理就是手動將區域性變數表中對應的的Slot清空。
置null操作意義不大,這通常會被編譯器優化掉。。。
最重要的一點!區域性變數表不像方法區中的類一樣有初始化賦值過程(準備階段),
因此,沒有賦初始值的區域性變數是不能使用的。不像類變數一樣有系統初始值。
2、運算元棧
運算元棧是方法執行的最基礎的支撐。
運算元棧中元素的資料型別要與位元組碼指令嚴格匹配,這在編譯時會保證,在類校驗階段還要再次驗證。
3、動態連結
指向方法區中執行時常量池中該棧幀所屬方法的引用,為了支援方法呼叫過程中的動態連結。
靜態解析:在類載入或第一次使用的時候就將符號引用轉換為直接引用。
動態連結:在執行期間才轉轉為直接引用。
4、方法返回地址
正常完成出口:方法正常執行退出
異常完成出口:。。。
方法退出過程就是將當前棧幀出棧,並恢復上層方法的區域性變數表和運算元棧,
把返回值壓入上層方法的運算元棧中,調整PC的值,指向下一條指令。
5、附加資訊
除錯資訊等。
3、方法呼叫
方法呼叫不等同於執行,呼叫只是確定是哪一個方法(引數、返回值、所屬類)。
1、解析
呼叫目標在編譯期就確定,這就是解析呼叫。
方法能解析的前提:方法在程式執行前就有一個可確定的呼叫版本,並且該版本在執行期不變。
符合該前提的方法主要包括靜態方法和私有方法。
靜態方法直接和類關聯,私有方法不可訪問,因此它們都不可通過繼承或其他方式重寫。
虛擬機器中的方法呼叫指令:
1、invokespecial:呼叫構造器<init>,私有方法和父類方法。
2、invokestatic:呼叫靜態方法。
3、invokevritual:呼叫虛方法
4、invokeinterface:呼叫介面方法
5、invokedynamic:動態解析呼叫方法。
只要能夠被1、2呼叫的方法都可以在解析時確定。
4、方法呼叫-分派
解析呼叫在編譯期完成,是靜態的。
分派則可以是靜態的也可以是動態的。
按照宗量數又可分為單分派和多分派。(方法接收者與引數統稱為方法宗量)
因此,就可組合出:動/靜態單/多分派 四種分派方式。
靜態分派是過載的虛擬機器層面的實現。動態分派是重寫的虛擬機器層面的實現。
1、靜態分派
Human man = new Man();
其中,Human稱為變數的靜態型別(Apparent Type),Man稱為變更量的實際型別(Actual Type)。
靜態型別在編譯時就可以確定,但是實際型別要在執行時才能確定。
其實,從英文名就很好理解,Apparent Type就是表面上的型別,Actual Type就是實際上的型別。
對於man,在編譯時就可以確定它是一個Human型別,但是,他到底是Man還是Woman要等程式執行時才知道。
方法被過載時,是通過靜態型別作為方法的選擇依據的,因此在編譯時就可以選定過載方法。
依據靜態型別來定位方法的執行版本的分派就稱為靜態分派。
所以,靜態分派不是虛擬機器做的,它是編譯期做的。
2、動態分派
既然靜態分派是在編譯期,那麼動態分派就在執行期咯。
void sayHello(Human human){ human.hello(); }
sayHello(man);
sayHello(woman);
對於上述程式碼,怎麼去確定human.hello()要呼叫的方法呢?
javap 反編譯後,發現它們都是由invokevirtual呼叫的,但是,兩個invokevirtual都是指向的Human的hello()。
但是兩個執行的方法明顯是不同的。
這就是因為invokevirtual指令的多型查詢過程:
1、找到運算元棧棧頂的元素指向的物件的實際型別,記為C。
都找到實際型別了,多型不就解決了。
2、在C中查詢與invokevirtual指令引數常量描述符和簡單名都相符的方法,
找到後,要檢查訪問許可權,許可權不通過,則丟擲IllegalAccessError異常。
3、否則,到繼承鏈上尋找。
4、否則,丟擲AbstractMethodError異常。
可以看出,invokevirtual指令的執行結果是和運算元棧的狀態相關的,
還可以看出,呼叫物件方法時,首先要做的,就是將物件引用入棧。
因此就多型就實現了。
3、單分派和多分派
方法的接收者與方法的引數統稱為方法的宗量。根據分派基於多少宗量,可以將分派劃分為單分派和多分派。
上面程式碼中,對 father.Chioce(new Candy());處程式碼 編譯期選擇依據兩點:
注意father的型別是可編譯時確定的。因此為靜態分派。
1、靜態型別是Father還是Son;
2、方法引數是Candy還是Fist。
基於兩個宗量進行的,因此靜態分派屬於多分派型別。
對son.Choice(new Candy()); 處呼叫:
son的型別在編譯期無法確定,因此為動態分派。
但是,此時編譯器已經指定了方法的引數必須是Candy型別的。
因此,動態分派時只需要確定方法的所屬類。
因此,Java的動態分派屬於單分派型別。
Java是靜態多分派,動態單分派的型別。
4、虛擬機器動態分派實現
出於效能考慮,在實現中,為類在方法區中建立了一個虛方法表(Virtual Method Table),
用於invokevirtual指令執行時,直接在該虛方法表中查詢方法。
虛方法表中存放著各個方法的實際入口地址,
如果子類沒有重寫父類方法,那麼子類的虛方法表中,該方法指向父類方法的實現入口。
如果子類重寫了,就指向子類自己的實現的入口。
為了實現方便,相同簽名的方法在子類和父類虛方法表中的索引都一樣。
虛方法表一般在類載入的連結階段初始化,就是在類第一次初始化之後。
為了invokeinterface執行,也建立了介面方法表(Interface Method Table)。
5、動態型別語言支援
動態型別語言可以實現在執行時自由地為類繫結欄位和方法,這就要求,在進行方法分派時,可以有自己的選擇。
但是目前講到的分派,方法分派時的查詢都是規定好了的。
因此,要支援動態型別支援,就要將方法分派的介面分享出來,讓我們可以自己去進行分派。
jdk1.7引入了java.lang.invoke包,提供了一種新的動態確定目標方法的機制:
MethodHandle
A method handle is a typed, directly executable reference to an underlying method, constructor, field,
or similar low-level operation, with optional transformations of arguments or return values.
也就是說,除了只能把類作為單獨實體來使用,我們可以通過MethodHandle將方法也抽象成一個單獨實體。
(雖然也是通過類來實現的。。。)
好了,我們現在能單獨使用方法了,但是,還得找到它吧。
這就涉及到怎麼確定一個方法:
1、方法所屬類
2、方法簡單名
3、方法描述符(引數,返回值)
MethodType
A method type represents the arguments and return type accepted and returned by a method handle,
or the arguments and return type passed and expected by a method handle caller.
MethodType封裝了對方法描述符的表示。
現在:
1、類可以用類的Class物件表示;
2、方法簡單名——字串
3、方法描述符——MethodType
就可以去找方法了。
MethodHandles類為我們提供了許多根據上述標識找方法的封裝。太貼心了。
invokedynamic指令:
同MethodHandle機制一樣,只是MethodHandle是上層實現,invokedynamic是底層實現。
每一處invokedynamic指令的位置都被稱作動態呼叫點(Dynamic Call Site)。
CallSite:
A CallSite
is a holder for a variable MethodHandle, which is called its target
.
An invokedynamic
instruction linked to a CallSite
delegates all calls to the site's current target.
invokedynamic指令的第一個引數不是CONSTANT_Methodref_info常量,
而是新增的CONSTANT_InvokeDynamic_info。
CONSTANT_InvokeDynamic_info包含三個資訊:
1、引導方法;
2、方法型別MethodType
3、方法名稱
根據前面分析,方法名稱、描述符有了,但是還差方法所屬類。所以,引導方法中,應該要提供查詢類!
引導方法(Bootstrap Method):
存放在BootstrapMethods屬性中,是有固定引數,並且返回值是java.lang.invoke.CallSite物件的方法。
代表真正要執行的目標方法呼叫。
根據CONSTANT_InvokeDynamic_info中的資訊,虛擬機器找到並執行引導方法,得到一個CallSite物件,
最終使用CallSite呼叫目標方法。
現在有了方法的標識,誰去幫我們找呢?
MethodHandles.Lookup lookup() :
Returns a Lookup object with full capabilities to emulate all supported bytecode behaviors of the caller.
Lookup物件可以模擬呼叫的位元組碼行為。就是它了。
6、 基於棧的位元組碼解釋執行引擎
主要注意,基於運算元棧,資料交換都要經過運算元棧。指令也是針對棧元素進行操作的。