Java具有三種特性:封裝、繼承、多態。java
Java文件在編譯過程當中不會進行傳統編譯的鏈接步驟,方法調用的目標方法以符號引用的方式存儲在Class文件中,這種多態特性給Java帶來了更靈活的擴展能力,但也使得方法調用變得相對複雜,須要在類加載期間,甚至到運行期間才能肯定目標方法的直接引用。jvm
全部方法調用的目標方法在Class文件裏面都是常量池中的符號引用。在類加載的解析階段,若是一個方法在運行以前有肯定的調用版本,且在運行期間不變,虛擬機會將其符號引用解析爲直接調用。ide
這種 編譯期可知,運行期不可變 的方法,主要包括靜態方法和私有方法兩大類,前者與具體類直接關聯,後者在外部不可訪問,二者都不能經過繼承或別的方式進行重寫性能
JVM提供了以下方法調用字節碼指令:spa
經過invokestatic和invokespecial指令調用的方法,能夠在解析階段肯定惟一的調用版本,符合這種條件的有靜態方法、私有方法、實例構造器和父類方法4種,它們在類加載時會把符號引用解析爲該方法的直接引用。code
public class VirtualTest { public static void hello() { System.out.println("hello"); } public static void main(String[] args) { hello(); } }
經過javap命令查看main方法字節碼對象
public class com.jvm.VirtualTest { public com.jvm.VirtualTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void hello(); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public static void main(java.lang.String[]); Code: 0: invokestatic #5 // Method hello:()V 3: return }
能夠發現hello方法是經過invokestatic指令調用的。blog
class VirtualTest { private int id; public static void main(String args[]) { new VirtualTest(); } }
public class com.jvm.VirtualTest { public com.jvm.VirtualTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class com/jvm/VirtualTest 3: dup 4: invokespecial #3 // Method "<init>":()V 7: pop 8: return }
能夠發現實例構造器是經過invokespecial指令調用的經過invokestatic和invokespecial指令調用的方法,能夠稱爲非虛方法,其他狀況稱爲虛方法,不過有一個特例,即被final關鍵字修飾的方法,雖然使用invokevirtual指令調用,因爲它沒法被覆蓋重寫,因此也是一種非虛方法繼承
非虛方法的調用是一個靜態的過程,因爲目標方法只有一個肯定的版本,因此在類加載的解析階段就能夠把符合引用解析爲直接引用,而虛方法的調用是一個分派的過程,有靜態也有動態,可分爲靜態單分派、靜態多分派、動態單分派和動態多分派。接口
靜態分派發生在代碼的編譯階段。
public class StaticDispatch { static abstract class Humnan {} static class Man extends Humnan {} static class Woman extends Humnan {} public void hello(Humnan guy) { System.out.println("hello, Humnan"); } public void hello(Man guy) { System.out.println("hello, Man"); } public void hello(Woman guy) { System.out.println("hello, Woman"); } public static void main(String[] args) { Humnan man = new Man(); Humnan woman = new Woman(); StaticDispatch dispatch = new StaticDispatch(); dispatch.hello(man); dispatch.hello(woman); } }
運行結果
hello, Humnan
hello, Humnan
相信有經驗的同窗看完代碼後就能得出正確的結果,但爲何會這樣呢?先看看main方法的字節碼指令
public class com.jvm.StaticDispatch { public com.jvm.StaticDispatch(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void hello(com.jvm.StaticDispatch$Humnan); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello, Humnan 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public void hello(com.jvm.StaticDispatch$Man); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String hello, Man 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public void hello(com.jvm.StaticDispatch$Woman); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String hello, Woman 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public static void main(java.lang.String[]); Code: 0: new #7 // class com/jvm/StaticDispatch$Man 3: dup 4: invokespecial #8 // Method com/jvm/StaticDispatch$Man."<init>":()V 7: astore_1 8: new #9 // class com/jvm/StaticDispatch$Woman 11: dup 12: invokespecial #10 // Method com/jvm/StaticDispatch$Woman."<init>":()V 15: astore_2 16: new #11 // class com/jvm/StaticDispatch 19: dup 20: invokespecial #12 // Method "<init>":()V 23: astore_3 24: aload_3 25: aload_1 26: invokevirtual #13 // Method hello:(Lcom/jvm/StaticDispatch$Humnan;)V //重點在這裏 29: aload_3 30: aload_2 31: invokevirtual #13 // Method hello:(Lcom/jvm/StaticDispatch$Humnan;)V //重點在這裏比較這兩行 34: return }
經過字節碼指令,能夠發現兩次hello方法都是經過invokevirtual指令進行調用,並且調用的是參數爲Human類型的hello方法。
Humnan man = new Man();
上述代碼中,變量man擁有兩個類型,一個靜態類型Human,一個實際類型Man,靜態類型在編譯期間可知。
在編譯階段,Java編譯器會根據參數的靜態類型決定調用哪一個重載版本,但在有些狀況下,重載的版本不是惟一的,這樣只能選擇一個「更加合適的版本」進行調用,因此不建議在實際項目中使用這種模糊的方法重載。
在運行期間根據參數的實際類型肯定方法執行版本的過程稱爲動態分派,動態分派和多態性中的重寫(override)有着緊密的聯繫。
public class DynamicDispatch { static abstract class Humnan { abstract void say(); } static class Man extends Humnan { @Override void say() { System.out.println("hello, i'm Man"); } } static class Woman extends Humnan { @Override void say() { System.out.println("hello, i'm Woman"); } } public static void main(String[] args) { Humnan man = new Man(); Humnan woman = new Woman(); man.say(); woman.say(); } }
運行結果:
hello, i'm Man
hello, i'm Woman
對於習慣了面向對象思惟的同窗對於這個結果應該是理所固然的。這種狀況下,顯然不能再根據靜態類型來決定方法的調用了,致使不一樣輸出結果的緣由很簡單,man和woman的實際類型不一樣,可是JVM如何根據實際類型決定須要調用哪一個方法?
main方法的字節碼指令
public static void main(java.lang.String[]); Code: 0: new #2 // class com/jvm/DynamicDispatch$Man 3: dup 4: invokespecial #3 // Method com/jvm/DynamicDispatch$Man."<init>":()V 7: astore_1 8: new #4 // class com/jvm/DynamicDispatch$Woman 11: dup 12: invokespecial #5 // Method com/jvm/DynamicDispatch$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method com/jvm/DynamicDispatch$Humnan.say:()V 20: aload_2 21: invokevirtual #6 // Method com/jvm/DynamicDispatch$Humnan.say:()V 24: return }
Humnan man = new Man(); Humnan woman = new Woman();
在Java堆上申請內存空間和實例化對象,並將這兩個實例的引用分別存放到局部變量表的第一、2位置的Slot中。
man.say(); woman.say();
16和20行指令分別把以前存放到局部變量表一、2位置的對象引用壓入操做數棧的棧頂,這兩個對象是執行say方法的接收者(Receiver),17和21行指令進行方法調用。
能夠發現,17和21兩條指令徹底同樣,但最終執行的目標方法卻不相同,這得從invokevirtual指令的多態查找提及了,invokevirtual指令在運行時分爲如下幾個步驟:
因此上述兩次invokevirtual指令將相同的符號引用解析成了不一樣對象的直接引用,這個過程就是Java語言中重寫的本質。
因爲動態分派是很是頻繁的動做,所以在虛擬機的實際實現中,會基於性能的考慮,並不會如此頻繁的搜索對應方法,通常會在方法區中創建一個虛方法表,使用虛方法表代替方法查詢以提升性能。
虛方法表在類加載的鏈接階段進行初始化,存放着各個方法的實際入口地址,若是某個方法在子類中沒有被重寫,那麼子類的虛方法表中該方法的入口地址和父類保持一致。
abstract class Humnan { abstract void say(); void run() { System.out.println("Human is run"); } } class Man extends Humnan { @Override void say() { System.out.println("hello, i'm Man"); } @Override void run() { System.out.println("Man is run"); } } class Woman extends Humnan { @Override void say() { System.out.println("hello, i'm Humnan"); } }
對應的虛方法表結構
因爲在Woman類中沒有重寫run方法,所以在Woman的虛方法表中,run方法直接指向Human實例