JVM總結-invokedynamic


前不久,“虛擬機”賽馬俱樂部來了個年輕人,標榜自己是動態語言,是先進分子。

這一天,先進分子牽着一頭鹿進來,說要參加賽馬。咱部里的老學究 Java 就不同意了呀,鹿又不是馬,哪能參加賽馬。

當然了,這種墨守成規的調用方式,自然是先進分子所不齒的。現在年輕人里流行的是鴨子類型(duck typing)[1],只要是跑起來像只馬的,它就是一只馬,也就能夠參加賽馬比賽。

class Horse {
  public void race() {
    System.out.println("Horse.race()"); 
  }
}
 
class Deer {
  public void race() {
    System.out.println("Deer.race()");
  }
}
 
class Cobra {
  public void race() {
    System.out.println("How do you turn this on?");
  }
}

(如何用同一種方式調用他們的賽跑方法?)

說到了這里,如果我們將賽跑定義為對賽跑方法(對應上述代碼中的 race())的調用的話,那么這個故事的關鍵,就在於能不能在馬場中調用非馬類型的賽跑方法。

為了解答這個問題,我們先來回顧一下 Java 里的方法調用。在 Java 中,方法調用會被編譯為 invokestatic,invokespecial,invokevirtual 以及 invokeinterface 四種指令。這些指令與包含目標方法類名、方法名以及方法描述符的符號引用捆綁。在實際運行之前,Java 虛擬機將根據這個符號引用鏈接到具體的目標方法。

可以看到,在這四種調用指令中,Java 虛擬機明確要求方法調用需要提供目標方法的類名。在這種體系下,我們有兩個解決方案。一是調用其中一種類型的賽跑方法,比如說馬類的賽跑方法。對於非馬的類型,則給它套一層馬甲,當成馬來賽跑。

另外一種解決方式,是通過反射機制,來查找並且調用各個類型中的賽跑方法,以此模擬真正的賽跑。

顯然,比起直接調用,這兩種方法都相當復雜,執行效率也可想而知。為了解決這個問題,Java 7 引入了一條新的指令 invokedynamic。該指令的調用機制抽象出調用點這一個概念,並允許應用程序將調用點鏈接至任意符合條件的方法上。

 作為 invokedynamic 的准備工作,Java 7 引入了更加底層、更加靈活的方法抽象 :方法句柄(MethodHandle)。

方法句柄的概念

方法句柄是一個強類型的,能夠被直接執行的引用 [2]。該引用可以指向常規的靜態方法或者實例方法,也可以指向構造器或者字段。當指向字段時,方法句柄實則指向包含字段訪問字節碼的虛構方法,語義上等價於目標字段的 getter 或者 setter 方法。

這里需要注意的是,它並不會直接指向目標字段所在類中的 getter/setter,畢竟你無法保證已有的 getter/setter 方法就是在訪問目標字段。

方法句柄的類型(MethodType)是由所指向方法的參數類型以及返回類型組成的。它是用來確認方法句柄是否適配的唯一關鍵。當使用方法句柄時,我們其實並不關心方法句柄所指向方法的類名或者方法名。

打個比方,如果兔子的“賽跑”方法和“睡覺”方法的參數類型以及返回類型一致,那么對於兔子遞過來的一個方法句柄,我們並不知道會是哪一個方法。

方法句柄的創建是通過 MethodHandles.Lookup 類來完成的。它提供了多個 API,既可以使用反射 API 中的 Method 來查找,也可以根據類、方法名以及方法句柄類型來查找。

當使用后者這種查找方式時,用戶需要區分具體的調用類型,比如說對於用 invokestatic 調用的靜態方法,我們需要使用 Lookup.findStatic 方法;對於用 invokevirutal 調用的實例方法,以及用 invokeinterface 調用的接口方法,我們需要使用 findVirtual 方法;對於用 invokespecial 調用的實例方法,我們則需要使用 findSpecial 方法。

調用方法句柄,和原本對應的調用指令是一致的。也就是說,對於原本用 invokevirtual 調用的方法句柄,它也會采用動態綁定;而對於原本用 invkespecial 調用的方法句柄,它會采用靜態綁定。

方法句柄同樣也有權限問題。但它與反射 API 不同,其權限檢查是在句柄的創建階段完成的。在實際調用過程中,Java 虛擬機並不會檢查方法句柄的權限。如果該句柄被多次調用的話,那么與反射調用相比,它將省下重復權限檢查的開銷

需要注意的是,方法句柄的訪問權限不取決於方法句柄的創建位置,而是取決於 Lookup 對象的創建位置

舉個例子,對於一個私有字段,如果 Lookup 對象是在私有字段所在類中獲取的,那么這個 Lookup 對象便擁有對該私有字段的訪問權限,即使是在所在類的外邊,也能夠通過該 Lookup 對象創建該私有字段的 getter 或者 setter。

由於方法句柄沒有運行時權限檢查,因此,應用程序需要負責方法句柄的管理。一旦它發布了某些指向私有方法的方法句柄,那么這些私有方法便被暴露出去了。

方法句柄的操作

方法句柄的調用可分為兩種,一是需要嚴格匹配參數類型的 invokeExact。它有多嚴格呢?假設一個方法句柄將接收一個 Object 類型的參數,如果你直接傳入 String 作為實際參數,那么方法句柄的調用會在運行時拋出方法類型不匹配的異常。正確的調用方式是將該 String 顯式轉化為 Object 類型。

在普通 Java 方法調用中,我們只有在選擇重載方法時,才會用到這種顯式轉化。這是因為經過顯式轉化后,參數的聲明類型發生了改變,因此有可能匹配到不同的方法描述符,從而選取不同的目標方法。調用方法句柄也是利用同樣的原理,並且涉及了一個簽名多態性(signature polymorphism)的概念。(在這里我們暫且認為簽名等同於方法描述符。)

方法句柄 API 有一個特殊的注解類 @PolymorphicSignature。在碰到被它注解的方法調用時,Java 編譯器會根據所傳入參數的聲明類型來生成方法描述符,而不是采用目標方法所聲明的描述符。

在剛才的例子中,當傳入的參數是 String 時,對應的方法描述符包含 String 類;而當我們轉化為 Object 時,對應的方法描述符則包含 Object 類。

如果你需要自動適配參數類型,那么你可以選取方法句柄的第二種調用方式 invoke。它同樣是一個簽名多態性的方法。invoke 會調用 MethodHandle.asType 方法,生成一個適配器方法句柄,對傳入的參數進行適配,再調用原方法句柄。調用原方法句柄的返回值同樣也會先進行適配,然后再返回給調用者。

方法句柄還支持增刪改參數的操作,這些操作都是通過生成另一個方法句柄來實現的。這其中,改操作就是剛剛介紹的 MethodHandle.asType 方法。刪操作指的是將傳入的部分參數就地拋棄,再調用另一個方法句柄。它對應的 API 是 MethodHandles.dropArguments 方法。

增操作則非常有意思。它會往傳入的參數中插入額外的參數,再調用另一個方法句柄,它對應的 API 是 MethodHandle.bindTo 方法。Java 8 中捕獲類型的 Lambda 表達式便是用這種操作來實現的,下一篇我會詳細進行解釋。

增操作還可以用來實現方法的柯里化 [3]。舉個例子,有一個指向 f(x, y) 的方法句柄,我們可以通過將 x 綁定為 4,生成另一個方法句柄 g(y) = f(4, y)。在執行過程中,每當調用 g(y) 的方法句柄,它會在參數列表最前面插入一個 4,再調用指向 f(x, y) 的方法句柄。

 

方法句柄的實現

下面我們來看看 HotSpot 虛擬機中方法句柄調用的具體實現。(由於篇幅原因,這里只討論 DirectMethodHandle。)

前面提到,調用方法句柄所使用的 invokeExact 或者 invoke 方法具備簽名多態性的特性。它們會根據具體的傳入參數來生成方法描述符。那么,擁有這個描述符的方法實際存在嗎?對 invokeExact 或者 invoke 的調用具體會進入哪個方法呢?

import java.lang.invoke.*;
 
public class Foo {
  public static void bar(Object o) {
    new Exception().printStackTrace();
  }
 
  public static void main(String[] args) throws Throwable {
    MethodHandles.Lookup l = MethodHandles.lookup();
    MethodType t = MethodType.methodType(void.class, Object.class);
    MethodHandle mh = l.findStatic(Foo.class, "bar", t);
    mh.invokeExact(new Object());
  }
}

結果:

$ java Foo
java.lang.Exception
        at Foo.bar(Foo.java:5)
        at Foo.main(Foo.java:12)

也就是說,invokeExact 的目標方法竟然就是方法句柄指向的方法。

先別高興太早。我剛剛提到過,invokeExact 會對參數的類型進行校驗,並在不匹配的情況下拋出異常。如果它直接調用了方法句柄所指向的方法,那么這部分參數類型校驗的邏輯將無處安放。因此,唯一的可能便是 Java 虛擬機隱藏了部分棧信息。

當我們啟用了 -XX:+ShowHiddenFrames 這個參數來打印被 Java 虛擬機隱藏了的棧信息時,你會發現 main 方法和目標方法中間隔着兩個貌似是生成的方法。

$ java -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames Foo
java.lang.Exception
        at Foo.bar(Foo.java:5)
        at java.base/java.lang.invoke.DirectMethodHandle$Holder. invokeStatic(DirectMethodHandle$Holder:1000010)
        at java.base/java.lang.invoke.LambdaForm$MH000/766572210. invokeExact_MT000_LLL_V(LambdaForm$MH000:1000019)
        at Foo.main(Foo.java:12)

總結與實踐

今天我介紹了 invokedynamic 底層機制的基石:方法句柄。

方法句柄是一個強類型的、能夠被直接執行的引用。它僅關心所指向方法的參數類型以及返回類型,而不關心方法所在的類以及方法名。方法句柄的權限檢查發生在創建過程中,相較於反射調用節省了調用時反復權限檢查的開銷。

方法句柄可以通過 invokeExact 以及 invoke 來調用。其中,invokeExact 要求傳入的參數和所指向方法的描述符嚴格匹配。方法句柄還支持增刪改參數的操作,這些操作是通過生成另一個充當適配器的方法句柄來實現的。

方法句柄的調用和反射調用一樣,都是間接調用,同樣會面臨無法內聯的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM