解析調用 方法調用的目標方法在Class文件里是一個常量池中的符號引用,在類載入的解析階段,將其中一部分符號引用轉化為直接引用,這種解析的前提是:方法在程式真正運行之前就有一個可確定的調用版本,並且這個方法的調用版本在運行期不可變(編譯期可知,運行器不可變)。這類方法的調用稱 ...
解析調用
方法調用的目標方法在Class文件里是一個常量池中的符號引用,在類載入的解析階段,將其中一部分符號引用轉化為直接引用,這種解析的前提是:方法在程式真正運行之前就有一個可確定的調用版本,並且這個方法的調用版本在運行期不可變(編譯期可知,運行器不可變)。這類方法的調用稱為解析。
Java虛擬機有5條方法調用的位元組碼指令:
- invokestatic:調用靜態方法。
- invokespecial:調用實例初始化
方法、私有方法和父類方法。 - invokevirtual:調用對象的實例方法。
- invokeinterface:調用介面方法。
- invokedynamic:調用以綁定invokedynamic指令的調用點對象作為目標的方法。
前4條指令,分派邏輯固化在Java虛擬機內部的,而invokedynamic的分派邏輯由用戶所設定的引導方法決定。
invokestatic和invokespecial調用的方法稱非虛方法,其他為虛方法(final方法除外)。因為final雖是invokevirtual調用的,但是它不能被覆蓋修改,所以它也是非虛方法。
分派調用
解析調用一定是靜態過程,在編譯期間就完全確定,不會延遲到運行期再去完成。而分派調用則可能是靜態的也可能是動態的,可分為單分派和多分派,兩兩組合構成靜態單分派、靜態多分派、動態單分派、動態多分派。
1、靜態分派(多分派)
依賴靜態類型來定位方法執行版本的分派稱為靜態分派。典型應用就是重載。靜態分派發生在編譯階段,分派動作不是虛擬機執行的。編譯期能確定一個”更加合適的“版本。
public class StaticDispatch {
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public static void sayHello(Human guy){
System.out.println("hello,guy!");
}
public static void sayHello(Man guy){
System.out.println("hello,gentlemen!");
}
public static void sayHello(Woman guy){
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man=new Man();
Human woman=new Woman();
sayHello(man);
sayHello(woman);
}
}
輸出
hello,guy!
hello,guy!
2、動態分派(單分派)
運行期根據實際類型確定方法執行版本的分派稱為動態分派。典型應用就是重寫。invokevirtual指令第一步就是在運行期確定接收者的實際類型,invokevirtual把常量池中的類方法符號引用解析到直接引用上。
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=new Woman();
man.sayHello();
}
}
輸出:
man say hello!
woman say hello!
woman say hello!
3、單分派和多分派
方法的接收者和方法的參數統稱為方法的宗量。單分派是根據一個宗量對目標方法進行選擇,多分派則是根據多於一個宗量對目標方法進行選擇。
public class Dispatcher {
static class QQ {}
static class _360 {}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose QQ");
}
public void hardChoice(_360 arg) {
System.out.println("father choose _360");
}
}
public static class Son extends Father {
@Override
public void hardChoice(QQ arg) {
System.out.println("son choose QQ");
}
@Override
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
輸出
father choose _360
son choose QQ
4、虛擬機動態分派的實現
基於性能的考慮,最常用的”穩定優化“手段就是為類在方法區建立一個虛方法表(Vritual Method Table,vtable,與此對應,介面中-Interface Method Table,itable),使用虛方法表索引來代替元數據查找以提高性能。
虛方法表存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表裡面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實現版本的入口地址。
為了程式實現上的方便,具有相同簽名的方法,在父類、子類的虛方法表中都應當具有一樣的索引序號,這樣當類型轉換時,僅需要變更查找的方法表,就可以從不同的虛方法表中按索引轉換出所需的入口地址。
方法表一般在類載入的連接階段初始化,準備了類的變數初始值後,虛擬機會把該類的方法表也初始化完畢。
虛擬機除了使用方法表外,還會使用內聯緩存和基於類型繼承關係分析技術的守護內聯兩種非穩定的激進優化手段來回去更高的性能,可參考晚期(運行期)優化。
參考資料:《深入理解Java虛擬機(第二版)》《Java虛擬機規範(Java SE 8版)》