JVM 方法調用之動態分派
1. 動態分派
一個體現是重寫(override)。下面的代碼,運行結果很明顯。
1 public class App { 2 3 public static void main(String[] args) { 4 Super object = new Sub(); 5 object.f(); 6 } 7 } 8 9 class Super {10 public void f() {11 System.out.println("super : f()");12 }13 14 public void f(int i) {15 System.out.println("super : f(int)");16 }17 }18 19 class Sub extends Super{20 21 @Override22 public void f() {23 System.out.println("sub : f()");24 }25 26 @Override27 public void f(int i) {28 System.out.println("sub : f(int)");29 }30 31 public void f(char c) {32 System.out.println("sub : f(char)");33 }34 }
最終輸出sub : f();
那麽虛擬機是怎麽做到動態分派的呢?
不同的虛擬機有不同的實現,最常用的是使用虛方法表(Virtual Method Table)
2. 虛方法表
對於Super和Sub類,虛方法表大致如下:(靈魂畫師)
上面的靈魂畫作是什麽意思呢?
虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表裏面的地址入口和父類相同簽名的方法的地址入口是一致的,都指向父類的實現入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換為向子類實現版本的入口地址。
從上圖主要得出幾個信息:
a. 上圖的大部分方法,子類Super和Sub均沒有重寫,那麽都指向父類Object的類型數據。f()和f(int)方法,父類子類都實現了,那麽兩者就指向不同的實現地址。f(char)只在子類定義實現,自然指向子類的類型數據。
b. 為了程序實現上的方便,具有相同簽名的方法,在父類,子類的虛方法表中都應當具有一樣的索引序號
3. 實例分析
以本文開頭的代碼進行分析。通過javap命令查看main方法的指令。
其中的invokevirtual指令詳細調用過程是這樣的:
1)指令中的#19指的是App類的常量池中第19個常量表的索引項。這個常量表(CONSTATN_Methodref_info)記錄的是方法f()信息的符號引用,JVM首先根據這個符號引用找到調用方法f()的類的全限定名com.khlin.Super,這是因為變量object被聲明為Super類型。
2) 在Super類型的方法表中查找方法f(),如果找到,則將方法f()在方法表中的索引項(具體值我不了解,這裏將其記為index) 記錄到App類的常量池中第19個常量表中(常量池解析)。因此,如果Super類型方法表中沒有f(),那麽即使Sub類型的方法表有該方法,也會報編譯失敗。
3)在調用invokevirtual指令前有一個aload_1指令,它會將開始創建中堆中的Sub對象的引用壓入操作數棧。然後invokevirtual指令會根據這個Sub對象的引用首先找到堆中的Sub對象,然後進一步找到Sub對象所屬類型的方法表。
4)這時,通過2)查找的index,可以定位到Sub類型方法表中的f()方法,然後通過直接地址找到該方法字節碼所在的內存空間。這就是父類和子類相同簽名的方法索引序號一致的用處。
4. 綜合考慮:一個可能想錯的例子
將本文開頭的代碼裏的main方法稍作修改,調用其他的方法。
1 public static void main(String[] args) {2 Super object = new Sub();3 char c = ‘a‘;4 object.f(c);5 }
結果將輸出sub : f(int)
明明Sub方法裏有完全一樣類型的f(char)方法,卻調用的是f(int).
相信通過前面的學習,已經可以明白原因了。
在object.f(c)調用時,虛擬機先到Super類的方法表裏,查找最為合適的方法。
Super類裏沒有剛好參數為char的f(char)方法,按照前面靜態分派和參數類型自動轉換的學習,可以知道,編譯器使用了除了f(char)之外最為合適的方法f(int)。獲取到索引後,通過索引到實際對象的Sub方法表裏找到f(int)方法,最終執行的就是Sub類的f(int)方法。
該方法的字節碼指令證明了上述的論證。
JVM 方法調用之動態分派