JVM 之(16)方法調用

前言

Java具有三種特性:封裝、繼承、多態。
Java文件在編譯過程當中不會進行傳統編譯的鏈接步驟,方法調用的目標方法以符號引用的方式存儲在Class文件中,這種多態特性給Java帶來了更靈活的擴展能力,但也使得方法調用變得相對複雜,須要在類加載期間,甚至到運行期間才能肯定目標方法的直接引用。

java

方法調用

全部方法調用的目標方法在Class文件裏面都是常量池中的符號引用。在類加載的解析階段,若是一個方法在運行以前有肯定的調用版本,且在運行期間不變,虛擬機會將其符號引用解析爲直接調用。緩存

這種 編譯期可知,運行期不可變 的方法,主要包括靜態方法和私有方法兩大類,前者與具體類直接關聯,後者在外部不可訪問,二者都不能經過繼承或別的方式進行重寫。app

JVM提供了以下方法調用字節碼指令:ide

  1. invokestatic:調用靜態方法;
  2. invokespecial:調用實例構造方法<init>,私有方法和父類方法;
  3. invokevirtual:調用虛方法;
  4. invokeinterface:調用接口方法,在運行時再肯定一個實現此接口的對象;
  5. invokedynamic:在運行時動態解析出調用點限定符所引用的方法以後,調用該方法;

經過invokestatic和invokespecial指令調用的方法,能夠在解析階段肯定惟一的調用版本,符合這種條件的有靜態方法、私有方法、實例構造器和父類方法4種,它們在類加載時會把符號引用解析爲該方法的直接引用。
函數

public class InvokestaticTest {
    
    InvokestaticTest(){
        
    }
    
    public static void sayHello() {
        System.out.println("hello");
    }
    public static void main(String args[]) {
        sayHello();
    }
}

javap  -verbose  InvokestaticTest.class
性能

能夠發現實例構造器是經過invokespecial指令調用的, sayHello方法是經過invokestatic指令調用的。
經過invokestatic和invokespecial指令調用的方法,能夠稱爲非虛方法,其他狀況稱爲虛方法,不過有一個特例,即被final關鍵字修飾的方法,雖然使用invokevirtual指令調用,因爲它沒法被覆蓋重寫,因此也是一種非虛方法。spa

非虛方法的調用是一個靜態的過程,因爲目標方法只有一個肯定的版本,因此在類加載的解析階段就能夠把符合引用解析爲直接引用,而虛方法的調用是一個分派的過程,有靜態也有動態,可分爲靜態單分派、靜態多分派、動態單分派和動態多分派。

orm

靜態分派

靜態分派發生在代碼的編譯階段。針對於方法的重載對象

public class StaticDispatch {

    static class Parent{}
    static class Child1 extends Parent{}
    static class Child2 extends Parent{}

    public void sayHello(Parent parent){
        System.out.println("parent sayHello");
    }
    public void sayHello(Child1 child1){
        System.out.println("child1 sayHello");
    }
    public void sayHello(Child2 child2){
        System.out.println("child2 sayHello");
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent parent1 = new Child1();
        Parent parent2 = new Child2();

        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.sayHello(parent);
        staticDispatch.sayHello(parent1);
        staticDispatch.sayHello(parent2);
        staticDispatch.sayHello((Child2)parent2);
    }
}
parent sayHello
parent sayHello
parent sayHello
child2 sayHello

javap  -verbose StaticDispatch.class
blog

經過字節碼指令,能夠發現四次hello方法都是經過invokevirtual指令進行調用,並且前三次調用的是參數爲Parent類型的sayHello方法,最後一次進行強轉後,調用Child2類型的sayHello方法。

再舉個例子,代碼以下:

public class StaticDispatchTest {

    public void sayHello(short word){
        System.out.println("short" + word);
    }
    public void sayHello(int word){
        System.out.println("int" + word);
    }
    public void sayHello(long word){
        System.out.println("long" + word);
    }
    public void sayHello(String word){
        System.out.println("String" + word);
    }
    public void sayHello(char word){
        System.out.println("char" + word);
    }
    public void sayHello(Character word){
        System.out.println("Character" + word);
    }
    public void sayHello(Object word){
        System.out.println("Object" + word);
    }
    public void sayHello(char ... word){
        System.out.println("char ..." + word);
    }

    public static void main(String[] args) {
        StaticDispatchTest staticDispatch = new StaticDispatchTest();
        staticDispatch.sayHello('a');
    }
}
chara

優先匹配到char方法,其次是int,long,Character, Objedt, char...

        在編譯階段,Java編譯器會根據參數的靜態類型決定調用哪一個重載版本,但在有些狀況下,重載的版本不是惟一的,這樣只能選擇一個「更加合適的版本」進行調用,因此不建議在實際項目中使用這種模糊的方法重載。


動態分派

         在運行期間根據參數的實際類型肯定方法執行版本的過程稱爲動態分派,動態分派和多態性中的重寫(override)有着緊密的聯繫
         因爲動態分派是很是頻繁的動做,所以在虛擬機的實際實現中,會基於性能的考慮,並不會如此頻繁的搜索對應方法,通常會在方法區中創建一個虛方法表,使用虛方法表代替方法查詢以提升性能。
        虛方法表在類加載的鏈接階段進行初始化,存放着各個方法的實際入口地址,若是某個方法在子類中沒有被重寫,那麼子類的虛方法表中該方法的入口地址和父類保持一致。
        一個類的方法表包含類的全部方法入口地址,從父類繼承的方法放在前面,接下來是接口方法和自定義的方法。若是某個方法在子類中沒有被重寫,那子類的虛方法表裏面的地址入口和父類相同的方法的入口地址一致。若是子類重寫了這個方法,子類方法表中的地址將會替換爲指向子類實現版本的入口地址。

public class DynamicDispatch {

    static class Parent{
        public void sayHello(){
            System.out.println("Parent");
        }
    }
    static class Child1 extends Parent {
        public void sayHello(){
            System.out.println("Child1");
        }
    }
    static class Child2 extends Parent {
        public void sayHello(){
            System.out.println("Child2");
        }
    }
    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent parent1 = new Child1();
        Parent parent2 = new Child2();

        parent.sayHello();
        parent1.sayHello();
        parent2.sayHello();
    }
}
Parent
Child1
Child2

javap  -verbose DynamicDispatch.class

能夠發現,2五、29和33的指令徹底同樣,但最終執行的目標方法卻不相同,這得從invokevirtual指令的多態查找提及了,invokevirtual指令在運行時分爲如下幾個步驟:

  1. 找到操做數棧的棧頂元素所指向的對象的實際類型,記爲C;
  2. 若是類型C中存在描述符和簡單名稱都相符的方法,則進行訪問權限驗證,若是驗證經過,則直接返回這個方法的直接引用,不然返回java.lang.IllegalAccessError異常;
  3. 若是類型C中不存在對應的方法,則按照繼承關係,從下往上依次對類型C的各父類進行搜索和驗證,進行第2步的操做;
  4. 若是各個父類也沒對應的方法,則拋出異常AbstractMethodError


invokevirtual和invokeinterface的區別

         虛函數表上的虛方法是按照從父類到子類的順序排序的,所以對於使用invokevirtual調用的虛函數,JVM徹底能夠在編譯期就肯定了虛函數在方法表上的offset,或者在首次調用以後就把這個offset緩存起來,這樣就能夠快速地從方法表中定位所要調用的方法地址。         然而對於接口類型引用,因爲一個接口能夠被不一樣的Class來實現,因此接口方法在不一樣類的方法表的offset固然就(極可能)不同了。所以,每次接口方法的調用,JVM都會搜尋一遍虛函數表,效率會比invokevirtual要低。

相關文章
相關標籤/搜索