天天看點

JVM--方法調用

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/qq_36367789/article/details/81711229

方法調用不是方法執行,方法調用是讓jvm确定調用哪個方法,是以,程式運作時的它是最普遍、最頻繁的操作。jvm需要在類加載期間甚至運作期間才能确定方法的直接引用。

解析

所有方法在Class檔案都是一個常量池中的符号引用,類加載的解析階段會将其轉換成直接引用,這種解析的前提是:要保證這個方法在運作期是不可變的。這類方法的調用稱為解析。

jvm提供了5條方法調用位元組碼指令:

  • [ ] invokestatic:調用靜态方法
  • [ ] invokespecial:調用構造器方法、私有方法和父類方法
  • [ ] invokevirtual:調用所有的虛方法。
  • [ ] invokeinterface:調用接口方法,會在運作時期再确定一個實作此接口的對象
  • [ ] invokedynamic: 現在運作時期動态解析出調用點限定符所引用的方法,然後再執行該方法,在此之前的4條指令,分派邏輯都是固化在虛拟機裡面的,而invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的。InvokeDynamic指令詳細請點選 InvokeDynamic指令

invokestatic

invokespecial

指令調用的方法,都能保證方法的不可變性,符合這個條件的有

靜态方法

私有方法

實力構造器

父類方法

4類。這些方法稱為非虛方法。

public class Main {
    public static void main(String[] args) {
        //invokestatic調用
        Test.hello();
        //invokespecial調用
        Test test = new Test();
    }
    static class Test{
        static void hello(){
            System.out.println("hello");
        }
    }
}           

解析調用一定是一個靜态的過程,在編譯期間就可以完全确定,在類裝載的解析階段就會把涉及的符号引用全部轉化為可确定的直接引用,不會延遲到運作期去完成。而分派調用可能是靜态的也可能是動态的,根據分派一句的宗量數可分為單分派和多分派。是以分派可分為:靜态單分派、靜态多分派、動态單分派、動态多分派。

靜态分派(方法重載)

所有依賴靜态類型來定位方法執行版本的分派動作成為靜态分派。
public class Test {
    static class Phone{}
    static class Mi extends Phone{}
    static class Iphone extends Phone{}

    public void show(Mi mi){
        System.out.println("phone is mi");
    }
    public void show(Iphone iphone){
        System.out.println("phone is iphone");
    }
    public void show(Phone phone){
        System.out.println("phone parent class be called");
    }

    public static void main(String[] args) {
        Phone mi = new Mi();
        Phone iphone = new Iphone();

        Test test = new Test();
        test.show(mi);
        test.show(iphone);
        test.show((Mi)mi);
    }
}           

執行結果:

phone parent class be called
phone parent class be called
phone is mi           

我們把上面代碼中的

Phone

稱為變量的靜态類型或者叫外觀類型,吧

Mi

Iphone

稱為實際類型,靜态類型僅僅在使用時發生變化,編譯可知;實際類型在運作期才知道結果,編譯器在編譯程式的時候并不知道一個對象的實際類型是什麼。

是以,jvm重載時是通過參數的靜态類型而不是實際類型作為判定依據。下圖可以證明:

根據上面的代碼也可以看出,我們可以使用強制類型轉換來使靜态類型發生改變。

動态分派(方法覆寫)

public class Test2 {
    static abstract class Phone{
        abstract void show();
    }
    static class Mi extends Phone{
        @Override
        void show() {
            System.out.println("phone is mi");
        }
    }
    static class Iphone extends Phone{
        @Override
        void show() {
            System.out.println("phone is iphone");
        }
    }

    public static void main(String[] args) {
        Phone mi = new Mi();
        Phone iphone = new Iphone();
        mi.show();
        iphone.show();
        mi = new Iphone();
        mi.show();
    }
}           
phone is mi
phone is iphone
phone is iphone           

這個結果大家肯定都能猜到,但是你又沒有想過編譯器是怎麼确定他們的實際變量類型的呢。這就關系到了

invokevirtual

指令,該指令的第一步就是在運作期确定接受者的實際類型。是以兩次調用

invokevirtual

指令吧常量池中的類方法符号引用解析到了不同的直接引用上。

invokevirtual

指令的運作時解析過程大緻分為以下幾個步驟。

(1)找到操作數棧頂的第一個元素(對象引用)所指向的對象的實際類型,記作C;

(2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行通路權限校驗,如果通過則傳回這個方法的直接引用,查找過程結束;如果不通過,則傳回

java.lang.IllegalAccessError。

(3)否則,按照繼承關系從下往上依次對C的各個父類進行第2步的搜尋和驗證。

(4)如果始終沒有找到合适的方法,則抛出

java.lang.AbstractMethodError異常

動态類型語言支援

動态語言的關鍵特征是它的類型檢查的主體過程是在運作期間而不是編譯期。相對的,在編譯期間進行類型檢查過程的語言(java、c++)就是靜态類型語言。

運作時異常:代碼隻要不運作到這一行就不會報錯。

連接配接時異常:類加載抛出異常。

那動态、靜态類型語言誰更好?

它們都有自己的優點。靜态類型語言在編譯期确定類型,可以提供嚴謹的類型檢查,有很多問題編碼的時候就能及時發現,利于開發穩定的大規模項目。動态類型語言在運作期确定類型,有很大的靈活性,代碼更簡潔清晰,開發效率高。

public class MethodHandleTest {
    static class ClassA {  
        public void show(String s) {
            System.out.println(s);  
        }  
    }  
    public static void main(String[] args) throws Throwable {  
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();  
        // 無論obj最終是哪個實作類,下面這句都能正确調用到show方法。
        getPrintlnMH(obj).invokeExact("fantj");
    }  
    private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
        // MethodType:代表“方法類型”,包含了方法的傳回值(methodType()的第一個參數)和具體參數(methodType()第二個及以後的參數)。   
        MethodType mt = MethodType.methodType(void.class, String.class);
        // lookup()方法來自于MethodHandles.lookup,這句的作用是在指定類中查找符合給定的方法名稱、方法類型,并且符合調用權限的方法句柄。   
        // 因為這裡調用的是一個虛方法,按照Java語言的規則,方法第一個參數是隐式的,代表該方法的接收者,也即是this指向的對象,這個參數以前是放在參數清單中進行傳遞,現在提供了bindTo()方法來完成這件事情。   
        return lookup().findVirtual(reveiver.getClass(), "show", mt).bindTo(reveiver);
    }  
}           
fantj           

無論obj是何種類型(臨時定義的ClassA抑或是實作PrintStream接口的實作類System.out),都可以正确調用到show()方法。

僅站在Java語言的角度看,MethodHandle的使用方法和效果上與Reflection都有衆多相似之處。不過,它們也有以下這些差別:

*

Reflection

MethodHandle

機制本質上都是在模拟方法調用,但是

Reflection

是在模拟Java代碼層次的方法調用,而MethodHandle是在模拟位元組碼層次的方法調用。在

MethodHandles.Lookup

上的三個方法

findStatic()

findVirtual()

findSpecial()

正是為了對應于

invokestatic

invokevirtual & invokeinterface

invokespecial

這幾條位元組碼指令的執行權限校驗行為,而這些底層細節在使用

Reflection API

時是不需要關心的。

Reflection

中的

java.lang.reflect.Method

對象遠比

MethodHandle

機制中的

java.lang.invoke.MethodHandle

對象所包含的資訊來得多。前者是方法在Java一端的全面映像,包含了方法的簽名、描述符以及方法屬性表中各種屬性的Java端表示方式,還包含有執行權限等的運作期資訊。而後者僅僅包含着與執行該方法相關的資訊。用開發人員通俗的話來講,

Reflection

是重量級,而

MethodHandle

是輕量級。

* 由于

MethodHandle

是對位元組碼的方法指令調用的模拟,那理論上虛拟機在這方面做的各種優化(如方法内聯),在

MethodHandle

上也應當可以采用類似思路去支援(但目前實作還不完善)。而通過反射去調用方法則不行。  

MethodHandle

Reflection

除了上面列舉的差別外,最關鍵的一點還在于去掉前面讨論施加的前提“僅站在Java語言的角度看”之後:

Reflection API

的設計目标是隻為Java語言服務的,而

MethodHandle

則設計為可服務于所有Java虛拟機之上的語言,其中也包括了Java語言而已。

invokedynamic指令

參考原文:

https://blog.csdn.net/a_dreaming_fish/article/details/50635651

一開始就提到了JDK 7為了更好地支援動态類型語言,引入了第五條方法調用的位元組碼指令invokedynamic,但前面一直沒有再提到它,甚至把之前使用MethodHandle的示例代碼反編譯後也不會看見invokedynamic的身影,它到底有什麼應用呢?

  某種程度上可以說

invokedynamic

指令與

MethodHandle

機制的作用是一樣的,都是為了解決原有四條

invoke*

指令方法分派規則固化在虛拟機之中的問題,把如何查找目标方法的決定權從虛拟機轉嫁到具體使用者代碼之中,讓使用者(包含其他語言的設計者)有更高的自由度。而且,它們兩者的思路也是可類比的,可以想象作為了達成同一個目的,一個用上層代碼和API來實作,另一個是用位元組碼和Class中其他屬性和常量來完成。是以,如果前面

MethodHandle

的例子看懂了,了解

invokedynamic

指令并不困難。

  每一處含有

invokedynamic

指令的位置都被稱作“動态調用點(Dynamic Call Site)”,這條指令的第一個參數不再是代表方法符号引用的

CONSTANT_Methodref_info

常量,而是變為JDK 7新加入的

CONSTANT_InvokeDynamic_info

常量,從這個新常量中可以得到3項資訊:引導方法(Bootstrap Method,此方法存放在新增的

BootstrapMethods

屬性中)、方法類型(MethodType)和名稱。引導方法是有固定的參數,并且傳回值是

java.lang.invoke.CallSite

對象,這個代表真正要執行的目标方法調用。根據

CONSTANT_InvokeDynamic_info

常量中提供的資訊,虛拟機可以找到并且執行引導方法,進而獲得一個CallSite對象,最終調用要執行的目标方法上。我們還是照例拿一個實際例子來解釋這個過程吧。如下面代碼清單所示:

public class InvokeDynamicTest {
    public static void main(String[] args) throws Throwable {  
        INDY_BootstrapMethod().invokeExact("icyfenix");  
    }
    public static void testMethod(String s) {
        System.out.println("hello String:" + s);  
    }
    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
        return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
    }
    private static MethodType MT_BootstrapMethod() {  
        return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
    }
    private static MethodHandle MH_BootstrapMethod() throws Throwable {
        return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());  
    }
    private static MethodHandle INDY_BootstrapMethod() throws Throwable {  
        CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));  
        return cs.dynamicInvoker();  
    }
}           
hello String:icyfenix           

BootstrapMethod()

,它的位元組碼很容易讀懂,所有邏輯就是調用

MethodHandles$Lookup的findStatic()

方法,産生

testMethod()

方法的

MethodHandle

,然後用它建立一個

ConstantCallSite

對象。最後,這個對象傳回給

invokedynamic

指令實作對

testMethod()

方法的調用,

invokedynamic

指令的調用過程到此就宣告完成了。