1. 一個方法調(diào)用的例子
public class CYoungMan extends YoungMan{
@Override
public String meet(Object obj) {
return "Object class";
}
@Override
public String meet(String str) {
return "String class";
}
public static void main(String[] args) {
Object str = "kat";
YoungMan ym = new CYoungMan();
String result = ym.meet(str);
}
}
在上面的例子中,對于方法調(diào)用ym.meet(str),我們大概知道有兩個階段:
(1)在編譯階段,根據(jù)方法接收者ym的靜態(tài)類型(static type)即YoungMan,以及參數(shù)str的靜態(tài)類型Object,來決定該方法調(diào)用的符號引用:YoungMan.meet:(Ljava/lang/Object;)Ljava/lang/String;(見javap的字節(jié)碼反匯編結(jié)果invokevirtual #7);
(2)在運行時階段,根據(jù)方法接收者ym的實際類型(actual type)即CYoungMan以及它的繼承層級,來確定方法實際引用。
上面的兩個階段分別是方法重載解析、動態(tài)分派。
說明:
1、 方法重載解析,國內(nèi)一般翻譯為靜態(tài)分派(相對于動態(tài)分派),但實際英文文檔是 Method Overload Resolution。 這里個人感覺按英文語義翻譯更好,所以采用這個。
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: ldc #4 // String kat
2: astore_1
3: new #5 // class method/invoke/CYoungMan
6: dup
7: invokespecial #6 // Method "<init>":()V
10: astore_2
11: aload_2
12: aload_1
13: invokevirtual #7 // Method method/invoke/YoungMan.meet:(Ljava/lang/Object;)Ljava/lang/String;
16: astore_3
17: return
...
2. 方法重載解析[1] (Method Overload Resolution)
可以看到,編譯階段能確定的、能使用的信息只有方法接收者或者入?yún)⒌撵o態(tài)類型,以及入?yún)㈧o態(tài)類型的繼承層次關(guān)系。對于一個確定的方法接收者及方法名的方法調(diào)用,JAVA編譯器會根據(jù)入?yún)磉x取一個重載方法,這過程有三個階段:
(1)不考慮基本類型的裝箱/拆箱、以及可變長參數(shù)這兩種情況,選取重載方法;
(2)如果在(1)找不到適配的方法,那么采用策略選取重載方法:允許裝箱/拆箱,但排除可變長參數(shù);
(3)如果在(2)找不到適配的方法,那么采用裝箱/拆箱與可變長參數(shù)組合使用的策略;
在任何一個階段找到重載方法的話,解析過程就結(jié)束了。
在JAVA語言規(guī)范里面,上面的過程稱之為“確認潛在的適配方法”(Identify Potentially Applicable Methods),也就是說,在任一階段可能找到多個候選的方法。
如果找到多個方法的話,再從中選擇最貼切的一個。這里利用的關(guān)鍵信息就是入?yún)㈧o態(tài)類型的繼承層次關(guān)系,原則就是優(yōu)先選擇繼承路徑最短的。舉個簡單的例子,現(xiàn)在有兩個候選方法:m1(TypeA a)、m1(TypeB b),而入?yún)⒌撵o態(tài)類型是TypeC,三種類型的繼承層次關(guān)系是TypeC->TypeB->TypeA,那么方法m1(TypeB b)肯定是最貼切的(最具體的),因為繼承路徑最短。
3. 動態(tài)分派 (Dynamic Dispatch)
對于重載解析中確定的方法,由于重寫(和繼承)特性而可能存在多個潛在的方法版本,必須在運行期按實際類型確定方法版本,即動態(tài)分派。
在運行期執(zhí)行時,JVM提供了有5種方法調(diào)用字節(jié)碼指令:
(1)invokestatic,調(diào)用靜態(tài)方法;
(2)invokespecial,調(diào)用實例構(gòu)造器<init>方法、私有方法和父類方法;
(3)invokevirtual,調(diào)用所有的虛方法;
(4)invokeinterface,調(diào)用接口方法;
(5)invokedynamic,調(diào)用動態(tài)方法;
前四種指令,均在類加載-解析階段將符號引用解析為方法的直接引用。至于最后的invokedynamic,是在JDK7中新增的指令,目的是在JVM中更好地運行動態(tài)類型語言。簡單說,就是在運行期能實現(xiàn)“重載方法解析+動態(tài)分派”兩重功能的方法解析調(diào)用。
其中,前兩個指令對應(yīng)的方法,因為潛在的方法版本唯一,直接引用是具體方法的指針,在執(zhí)行指令可以直接訪問了,這些方法也稱之為非虛方法。相反,第3、4個指令,虛方法及接口方法,得到的直接引用只是方法表的一個索引值,執(zhí)行指令時還需要進一步解析,這里有兩個階段。
(1)非接口方法解析(類加載-解析階段)
這里說下非接口方法符號引用解析為直接引用的過程。假設(shè)目標符號引用為類C,則JVM的步驟如下:
- 在C中查找符合名字及描述符的方法,如果有則返回方法直接引用(方法指針),結(jié)束;
- 如果1沒有找到,按繼承層次從低往高,先從C的父類中繼續(xù)搜索,直至Object類,如果有則返回方法直接引用(方法表索引值),結(jié)束;
- 如果2沒有找到,在C實現(xiàn)的接口列表及他們父接口中搜索,如果找到,還要滿足一條限制【若C不是抽象類,結(jié)果必須不能為抽象方法,不然報錯AbstractMethodError】才能返回。
- 如果最后都沒找到,報錯NoSuchMethodError。
當然,過程找到的方法必須具備訪問權(quán)限了,不然報錯IllegalAccessError。接口方法解析也是類似的原理。
(2)invokevirtual 的解析過程(指令執(zhí)行階段)
- 確定操作數(shù)棧頂?shù)谝粋€元素的所指向的對象實際類型,設(shè)為C;
- 在C中查找符合名字及描述符的方法,如果有則返回方法直接引用(方法指針),結(jié)束;
- 如果1沒有找到,按繼承層次從低往高,先從C的父類中繼續(xù)搜索,直至Object類,如果有則返回方法直接引用(方法指針),結(jié)束;
這個過程其實跟類加載的方法解析是類似的,都是在類繼承層次上按圖索驥??梢钥吹剑@里涉及逐層的搜索,性能是堪憂的,因此在虛擬機的實際實現(xiàn)中會采用空間換時間的策略,構(gòu)建虛方法表(對于invokeinterface,則是接口方法表),使用虛方法表索引來替代層層搜索。
4. 方法表
在前面提到,方法表是用來加速類加載階段的方法解析、虛方法/接口方法指令的解析過程的,所以方法表的構(gòu)建一定是在類加載過程的解析階段之前。
你大概猜到了,是的,是在準備階段,除了為靜態(tài)字段分配內(nèi)存外,還構(gòu)造了類的方法表。
方法表[2]的構(gòu)建過程就不贅言了,下面列一下它的兩點特征:
- 本質(zhì)上是一個數(shù)組,元素指向當前類或者祖先類的方法(方法引用);
- 子類方法表包含父類方法表中所有方法,如果某個方法在子類中沒有被重寫,則方法引用與父類的一致,否則用自己實現(xiàn)的方法替換父類的;
相當于把一個分層結(jié)構(gòu)水平鋪開,無論類加載的方法解析還是虛方法調(diào)用指令的解析,只需要在當前實際類型的方法表中遍歷搜索即可,不需要層層遞歸搜索了。
問題與討論:
1、對包裝類型拆箱,不考慮空指針問題,相當于把問題拋給coder?