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é)碼反匯編結果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)類型的繼承層次關系节预。對于一個確定的方法接收者及方法名的方法調(diào)用叶摄,JAVA編譯器會根據(jù)入?yún)磉x取一個重載方法,這過程有三個階段:
(1)不考慮基本類型的裝箱/拆箱安拟、以及可變長參數(shù)這兩種情況准谚,選取重載方法;
(2)如果在(1)找不到適配的方法去扣,那么采用策略選取重載方法:允許裝箱/拆箱,但排除可變長參數(shù)樊破;
(3)如果在(2)找不到適配的方法愉棱,那么采用裝箱/拆箱與可變長參數(shù)組合使用的策略;
在任何一個階段找到重載方法的話哲戚,解析過程就結束了奔滑。
在JAVA語言規(guī)范里面,上面的過程稱之為“確認潛在的適配方法”(Identify Potentially Applicable Methods)顺少,也就是說朋其,在任一階段可能找到多個候選的方法王浴。
如果找到多個方法的話,再從中選擇最貼切的一個梅猿。這里利用的關鍵信息就是入?yún)㈧o態(tài)類型的繼承層次關系氓辣,原則就是優(yōu)先選擇繼承路徑最短的。舉個簡單的例子袱蚓,現(xiàn)在有兩個候選方法:m1(TypeA a)
钞啸、m1(TypeB b)
,而入?yún)⒌撵o態(tài)類型是TypeC
喇潘,三種類型的繼承層次關系是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)用實例構造器<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)用。
其中姊扔,前兩個指令對應的方法惠奸,因為潛在的方法版本唯一,直接引用是具體方法的指針恰梢,在執(zhí)行指令可以直接訪問了佛南,這些方法也稱之為非虛方法梗掰。相反,第3嗅回、4個指令及穗,虛方法及接口方法,得到的直接引用只是方法表的一個索引值妈拌,執(zhí)行指令時還需要進一步解析拥坛,這里有兩個階段。
(1)非接口方法解析(類加載-解析階段)
這里說下非接口方法符號引用解析為直接引用的過程尘分。假設目標符號引用為類C猜惋,則JVM的步驟如下:
- 在C中查找符合名字及描述符的方法,如果有則返回方法直接引用(方法指針)培愁,結束著摔;
- 如果1沒有找到,按繼承層次從低往高定续,先從C的父類中繼續(xù)搜索谍咆,直至Object類,如果有則返回方法直接引用(方法表索引值)私股,結束摹察;
- 如果2沒有找到,在C實現(xiàn)的接口列表及他們父接口中搜索倡鲸,如果找到供嚎,還要滿足一條限制【若C不是抽象類,結果必須不能為抽象方法峭状,不然報錯AbstractMethodError】才能返回克滴。
- 如果最后都沒找到,報錯NoSuchMethodError优床。
當然劝赔,過程找到的方法必須具備訪問權限了,不然報錯IllegalAccessError胆敞。接口方法解析也是類似的原理着帽。
(2)invokevirtual 的解析過程(指令執(zhí)行階段)
- 確定操作數(shù)棧頂?shù)谝粋€元素的所指向的對象實際類型,設為C移层;
- 在C中查找符合名字及描述符的方法仍翰,如果有則返回方法直接引用(方法指針),結束幽钢;
- 如果1沒有找到,按繼承層次從低往高傅是,先從C的父類中繼續(xù)搜索匪燕,直至Object類蕾羊,如果有則返回方法直接引用(方法指針),結束帽驯;
這個過程其實跟類加載的方法解析是類似的龟再,都是在類繼承層次上按圖索驥∧岜洌可以看到利凑,這里涉及逐層的搜索,性能是堪憂的嫌术,因此在虛擬機的實際實現(xiàn)中會采用空間換時間的策略哀澈,構建虛方法表(對于invokeinterface,則是接口方法表)度气,使用虛方法表索引來替代層層搜索割按。
4. 方法表
在前面提到,方法表是用來加速類加載階段的方法解析磷籍、虛方法/接口方法指令的解析過程的适荣,所以方法表的構建一定是在類加載過程的解析階段之前。
你大概猜到了院领,是的弛矛,是在準備階段,除了為靜態(tài)字段分配內(nèi)存外比然,還構造了類的方法表丈氓。
方法表[2]的構建過程就不贅言了,下面列一下它的兩點特征:
- 本質(zhì)上是一個數(shù)組谈秫,元素指向當前類或者祖先類的方法(方法引用)扒寄;
- 子類方法表包含父類方法表中所有方法,如果某個方法在子類中沒有被重寫拟烫,則方法引用與父類的一致该编,否則用自己實現(xiàn)的方法替換父類的;
相當于把一個分層結構水平鋪開硕淑,無論類加載的方法解析還是虛方法調(diào)用指令的解析课竣,只需要在當前實際類型的方法表中遍歷搜索即可,不需要層層遞歸搜索了置媳。
問題與討論:
1于樟、對包裝類型拆箱,不考慮空指針問題拇囊,相當于把問題拋給coder迂曲?