JVM如何處理方法呼叫

微笑面對生活發表於2018-08-14

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

解析

所有方法在Class檔案都是一個常量池中的符號引用,類載入的解析階段會將其轉換成直接引用,這種解析的前提是:要保證這個方法在執行期是不可變的。這類方法的呼叫稱為解析。

jvm提供了5條方法呼叫位元組碼指令:

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

invokestaticinvokespecial指令呼叫的方法,都能保證方法的不可變性,符合這個條件的有靜態方法私有方法實力構造器父類方法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");
        }
    }
}
複製程式碼

JVM如何處理方法呼叫

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

靜態分派(方法過載)

所有依賴靜態型別來定位方法執行版本的分派動作成為靜態分派。

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稱為變數的靜態型別或者叫外觀型別,吧MiIphone稱為實際型別,靜態型別僅僅在使用時發生變化,編譯可知;實際型別在執行期才知道結果,編譯器在編譯程式的時候並不知道一個物件的實際型別是什麼。

所以,jvm過載時是通過引數的靜態型別而不是實際型別作為判定依據。下圖可以證明:

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指令吧常量池中的類方法符號引用解析到了不同的直接引用上。

JVM如何處理方法呼叫

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都有眾多相似之處。不過,它們也有以下這些區別

  • ReflectionMethodHandle機制本質上都是在模擬方法呼叫,但是Reflection是在模擬Java程式碼層次的方法呼叫,而MethodHandle是在模擬位元組碼層次的方法呼叫。在MethodHandles.Lookup上的三個方法findStatic()findVirtual()findSpecial()正是為了對應於invokestaticinvokevirtual & invokeinterfaceinvokespecial這幾條位元組碼指令的執行許可權校驗行為,而這些底層細節在使用Reflection API時是不需要關心的。
  • Reflection中的java.lang.reflect.Method物件遠比MethodHandle機制中的java.lang.invoke.MethodHandle物件所包含的資訊來得多。前者是方法在Java一端的全面映像,包含了方法的簽名、描述符以及方法屬性表中各種屬性的Java端表示方式,還包含有執行許可權等的執行期資訊。而後者僅僅包含著與執行該方法相關的資訊。用開發人員通俗的話來講,Reflection重量級,而MethodHandle輕量級
  • 由於MethodHandle是對位元組碼的方法指令呼叫的模擬,那理論上虛擬機器在這方面做的各種優化(如方法內聯),在MethodHandle上也應當可以採用類似思路去支援(但目前實現還不完善)。而通過反射去呼叫方法則不行。  
  • MethodHandleReflection除了上面列舉的區別外,最關鍵的一點還在於去掉前面討論施加的前提“僅站在Java語言的角度看”之後:Reflection API的設計目標是隻為Java語言服務的,而MethodHandle則設計為可服務於所有Java虛擬機器之上的語言,其中也包括了Java語言而已。
invokedynamic指令

參考原文:blog.csdn.net/a_dreaming_…

一開始就提到了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指令的呼叫過程到此就宣告完成了。

重點參考:《深入java虛擬機器第二版》

相關文章