一、問題
Java方法調用過程中,Jvm是如何知道調用的是哪個類的方法?Jvm又是如何處理?
二、概念
a、當子類和父類(接口和實現類)存在同一個方法時,子類重寫父類(接口)方法時,程序在運行時調用的方法時,是調用父類(接口)的方法呢?還是調用子類的方法呢?我們將確定這種調用何種方法的操作稱之為綁定。
綁定又分為靜態綁定和動態綁定。
靜態綁定
靜態綁定是在程序執行前就已經被綁定了(也就是在程序編譯過程中就已經知道這個方法是哪個類中的方法)。
public class StaticBindDemo { public static void s1() { System.out.println("static s1"); } private void p1() { System.out.println("private p1"); } public final void f1() { System.out.println("final f1"); } }
調用方:
public class StaticCall { public static void main(String[] args) { StaticBindDemo sbd = new StaticBindDemo(); StaticBindDemo.s1(); sbd.f1(); } }
反編譯后的文件:


上面的源代碼反編譯后,我們可以看到
調用的是靜態方法
8: invokestatic #4 // Method com/jstar/jvm/sync/bind/StaticBindDemo.s1:()V
1、#4指的是常量沲中的第4個常量表索引項,記錄的是方法s1的符號引用,jvm會根據這個符號引用找到方法f1
所在的類的全限定名: com/jstar/jvm/sync/bind/StaticBindDemo
2、緊接着JVM會加載、邊接和初始化類StaticBindDemo類
3、然后在StaticBindDem類所在的方法區中找到s1()方法的直接地址,並將這個直接地址記錄到StaticCall類的常量池索引為4的常量表中。這個過程叫常量池解析 ,以后再次調用StaticBindDemo.s1時,將直接找到s1方法的字節碼;
4、 完成了StaticCall類常量池索引項4的常量表的解析之后,JVM就可以調用s1()方法,並開始解釋執行f1()方法中的指令了。
通過上面的過程,我們發現經過常量池解析之后,JVM就能夠確定要調用的s1()方法具體在內存的什么位置上了。實際上,這個信息在編譯階段就已經在StaticCall類的常量池中記錄了下來。這種在編譯階段就能夠確定調用哪個方法的方式,我們叫做靜態綁定機制
注:Java中只有private、static和final修飾的方法以及構造方法是靜態綁定。
a、private方法的特點是不能被繼承,也就是不存在調用其子類的對象,只能調用對象自身,因此private方法和定義該方法的類綁定在一起。
b、static方法又稱類方法,類方法屬於類文件。它不依賴對象而存在,在調用的時候就已經知道是哪個類的,所以是類方法是屬於靜態綁定。
c、final方法:final方法可以被繼承,但是不能被重寫,所以也就是說final方法是屬於靜態綁定的,因為調用的方法是一樣的。
總結:如果一個方法不可被繼承或者繼承后不可被覆蓋,那么這個方法就采用的靜態綁定。
動態綁定
編譯器在每次調用方法時都要進行搜索,時間開銷相當大。因此虛擬機會預先為每個類創建一個方發表(method table),其中列出了所有方法的簽名和實際調用的方法。
public class Father { public void f1() { System.out.println("father-f1"); } public void f1(int i) { System.out.println("father-f1 params-int :" + i); } } public class Son extends Father{ public void f1() { System.out.println("son f1"); } public void f1(char c) { System.out.println("son-f1 params-c:" + c); } } public class Demo { public static void main(String[] args) { Father f = new Son(); f.f1(); //f.f1('c'); } }
通過反編譯Demo我們來看看jvm是怎么執行的

其中invokevirtual指令的詳細調用過程是這樣的:
(1) invokevirtual指令中的#4指的是Demo類的常量池中第4個常量表的索引項。這個常量表(Methodref ) 記錄的是方法f1信息的符號引用(包括f1所在的類名,方法名和返回類型)。JVM會首先根據這個符號引用找到調用方法f1的類的全限定名: Father。這是因為調用方法f1的類的對象father聲明為Father類型。
(2) 在Father類型的方法表中查找方法f1,如果找到,則將方法f1在方法表中的索引項記錄到Demo類的常量池中第4個常量表中(常量池解析 )。這里有一點要注意:如果Father類型方法表中沒有方法f1,那么即使Son類型中方法表有,編譯的時候也通過不了。因為調用方法f1的類的對象father的聲明為Father類型。
(3) 在調用invokevirtual指令前有一個aload_1指令,它會將開始創建在堆中的Son對象的引用壓入操作數棧。然后invokevirtual指令會根據這個Son對象的引用首先找到堆中的Son對象,然后進一步找到Son對象所屬類型的方法表.

(4) 這是通過第(2)步中解析完成的#4常量表中的方法表的索引項,可以定位到Son類型方法表中的方法f1(),然后通過直接地址找到該方法字節碼所在的內存空間。
很明顯,根據對象(father)的聲明類型(Father)還不能夠確定調用方法f1的位置,必須根據father在堆中實際創建的對象類型Son來確定f1方法所在的位置。這種在程序運行過程中,通過動態創建的對象的方法表來定位方法的方式,我們叫做 動態綁定機制 。
動態綁定過程:
<1>虛擬機提取對象的實際類型的方法表。
<2>虛擬機搜索方法簽名,此時虛擬機已經知道應該調用哪種方法。(PS:方法的簽名包括了:1.方法名 2.參數的數量和類型~~~~返回類型不是簽名的一部分。)
<3>虛擬機調用方法
PS:由於動態綁定需要在運行時確定執行哪個版本的方法實現或者變量,比起靜態綁定起來要耗時。所以在不影響整體設計,我們可以考慮將方法或者變量使用private,static或者final進行修飾。這邊優化的內容就涉及到了內聯的知識(我們在Java方法內聯中專門介紹)。