Invokedynamic-Java的祕密武器

最先關於invokedynamic的工做至少能夠追溯到2007年,首次成功進行的動態調用是在2008年8月26日進行的。這早於Sun被Oracle收購以前,而且按照大多數開發人員的標準,該功能已經開發了很長時間。 。java

invokedynamic的卓越之處在於它是自Java 1.0之後的第一個新增的字節碼。它加入了現有的調用字節碼invokevirtual,invokestatic,invokeinterface和invokespecial。這四個現有操做碼實現了Java開發人員一般熟悉的全部形式的方法分派,特別是:編程

  • invokevirtual -實例方法的標準調用
  • invokestatic -用於分派靜態方法
  • invokeinterface -用於經過接口調用方法
  • invokespecial -在須要非虛擬(即「精確」)調度時使用

一些開發人員可能對平臺爲什麼須要所有四個操做碼感到好奇,因此讓咱們看一個使用不一樣的調用操做碼的簡單示例,以說明它們之間的區別:bootstrap

public class InvokeExamples {
    public static void main(String[] args) {
        InvokeExamples sc = new InvokeExamples();
        sc.run();
    }

    private void run() {
        List<String> ls = new ArrayList<>();
        ls.add("Good Day");

        ArrayList<String> als = new ArrayList<>();
        als.add("Dydh Da");
    }
}

這將產生字節碼,咱們可使用javap工具將其反彙編:安全

javap -c InvokeExamples.class

結果輸出:框架

public class kathik
.
InvokeExamples {
  public kathik.InvokeExamples();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class kathik/InvokeExamples
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokespecial #4                  // Method run:()V
      12: return

  private void run();
    Code:
       0: new           #5                  // class java/util/ArrayList
       3: dup
       4: invokespecial #6                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #7                  // String Good Day
      11: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: new           #5                  // class java/util/ArrayList
      20: dup
      21: invokespecial #6                  // Method java/util/ArrayList."<init>":()V
      24: astore_2
      25: aload_2
      26: ldc           #9                  // String Dydh Da
      28: invokevirtual #10                 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      31: pop
      32: return
}

這展現了四種調用操做碼中的三種(其他一種,invokestatic 是微不足道的擴展)。首先,咱們能夠看到兩個調用(在run方法的字節11和28處):函數

ls.add("Good Day")

工具

als.add("Dydh Da")

在Java源代碼中看起來很是類似,但實際上在字節碼中的表示方式有所不一樣。性能

對於javac,變量ls的靜態類型爲List<String>,而 List 是接口。所以,還沒有在編譯時肯定 add 方法在運行時方法表中的精確位置(一般稱爲 「vtable」)。所以,源代碼編譯器將發出 invokeinterface 指令,並將該方法的實際查找推遲到運行時,直到能夠檢查 ls 的實際 vtable 並找到 add 方法的位置爲止。this

相反,該調用als.add("Dydh Da")被 als 接收,而且此類型的靜態類型是類類型 - ArrayList<String>。這意味着在編譯時已知方法在 vtable 中的位置。所以,javac 可以爲確切的 vtable 條目發出 invokevirtual 指令。方法的最終選擇雖然仍在運行時肯定,由於這容許方法被覆蓋,可是 vtable 插槽是在編譯時肯定的。設計

不只如此,該示例還顯示了 invokespecial 的兩種可能的用例。在應在運行時準確肯定調度的狀況下使用此操做碼,尤爲是既不但願也不可能進行方法覆蓋的狀況。該示例演示的兩種狀況是私有方法和父類調用(Object的構造函數),由於此類方法在編譯時是已知的,不能被覆蓋。

精明的讀者會注意到,全部對 Java 方法的調用都被編譯爲這四個操做碼之一,所以出現了問題 - invokedynamic 的做用是什麼,爲何對Java開發人員有用?

這些功能的主要目標是建立一個字節碼來處理一種新的方法分派,本質上,它容許應用程序級代碼肯定調用將執行的方法,而且僅在調用即將執行時才這樣作。與之前提供的Java平臺相比,這使得語言和框架編寫者能夠支持更多的動態編程樣式。

目的是用戶代碼使用API方法來肯定運行時的調用,而不會遭受因反射產生的性能損失和與之相關的安全問題。實際上,一旦功能充分紅熟,invokedynamic 的既定目標將與常規方法調度(invokevirtual)同樣快。

當 Java 7 到來時,JVM 增長了支持執行新的字節碼,可是不管提交的什麼樣的 Java 代碼,javac 都不會產生包含invokedynamic 的字節碼。相反,該功能僅用於支持 JVM 上運行的 JRuby 和其餘動態語言。

這在 Java 8 中有所改變,在 Java 8 中,如今已經生成了 invokedynamic,並在後臺使用它來實現 lambda 表達式和默認方法,以及 Nashorn 的主要調度機制。可是,Java應用程序開發人員仍然沒有直接方法來進行徹底動態的方法解析。也就是說,Java語言沒有可建立通用的 invokedynamic 調用點的關鍵字或庫。這意味着儘管它提供了強大的功能,可是對於大多數 Java 開發人員來講,該機制仍然晦澀難懂。讓咱們看看如何在咱們的代碼中利用它。

方法句柄簡介

爲了使 invokedynamic 正常工做,關鍵概念是方法句柄。這是表示應該從 invokedynamic 調用點調用的方法的一種方式。通常的想法是,每一個 invokedynamic 指令都與一個特殊的方法(稱爲引導方法或BSM - bootstrap method)相關聯。當解釋器到達 invokedynamic 指令時,將調用BSM,BSM返回一個對象(包含方法句柄),該對象指示調用點應實際執行的方法。

這有點相似於反射,可是反射具備侷限性,使其不適合與 invokedynamic 一塊兒使用。相反,將java.lang.invoke.MethodHandle(和子類)添加到 Java 7 API 中,以表示 invokedynamic 能夠定位的方法。MethodHandle 類從 JVM 接受一些特殊處理,以使其正確運行。

能夠把方法句柄想成是一種方法,一種安全,現代的方式完成核心反射,並儘量實現最大的類型安全性。它們對於invokedynamic 是必需的,但也能夠獨立使用。

方法類型

Java方法能夠認爲由四個基本部分組成:

  • Name 名稱
  • Signature 簽名(包括返回類型)
  • 定義的類別 Class
  • 實現該方法的 Bytecode 字節碼

這意味着,若是要引用方法,則須要一種有效地表示方法簽名的方法(而不是使用必須使用反射的可怕的Class<?>[]技巧)。

換句話說,方法句柄所需的第一個構建塊是一種表示要查找的方法簽名的方法。在 Java 7 中引入的方法句柄API中,此角色由java.lang.invoke.MethodType類完成,該類使用不可變的實例來表示簽名。要獲取 MethodType,請使用methodType() 工廠方法。這是一個可變參數方法,將類對象做爲參數。

第一個參數是與簽名的返回類型相對應的類對象。其他參數是與簽名中的方法參數類型相對應的類對象。例如:

// Signature of toString()
MethodType mtToString = MethodType.methodType(String.class);

// Signature of a setter method
MethodType mtSetter = MethodType.methodType(void.class, Object.class);

// Signature of compare() from Comparator<String>
MethodType mtStringComparator = MethodType.methodType(int.class, String.class, String.class);

使用 MethodType,咱們如今可使用它,以及定義方法以查找方法句柄的名稱和類。爲此,咱們須要調用靜態MethodHandles.lookup() 方法。這爲咱們提供了一個「查找上下文」,該上下文基於當前正在執行的方法(即調用lookup() 的方法)的訪問權限。

查找上下文對象具備許多名稱以 「find」 開頭的方法,例如 findVirtual(),findConstructor(),findStatic()。這些方法將返回實際的方法句柄,但前提是查找上下文是在能夠訪問(調用)所請求方法的方法中建立的。與反射不一樣,沒有辦法破壞此訪問控制。換句話說,方法句柄不具備setAccessible() 方法的等效項。例如:

public MethodHandle getToStringMH() {
    MethodHandle mh = null;
    MethodType mt = MethodType.methodType(String.class);
    MethodHandles.Lookup lk = MethodHandles.lookup();

    try {
        mh = lk.findVirtual(getClass(), "toString", mt);
    } catch (NoSuchMethodException | IllegalAccessException mhx) {
        throw (AssertionError)new AssertionError().initCause(mhx);
    }

    return mh;
}

MethodHandle上有兩種方法可用於調用方法句柄invoke() 和 +invokeExact()。兩種方法都將接收方參數和調用參數做爲參數,所以簽名爲:

public final Object invoke(Object... args) throws Throwable;
public final Object invokeExact(Object... args) throws Throwable;

二者之間的區別在於invokeExact() 會嘗試使用提供的精確參數直接調用方法句柄。另外一方面,invoke() 能夠根據須要稍微更改方法參數。invoke() 執行asType() 轉換,該轉換能夠根據如下規則集轉換參數:

  • 若是須要,將對基本類型裝箱
  • 若是須要,裝箱的基本類型將被取消裝箱
  • 必要時將擴大基本類型
  • void 返回類型將轉換爲0(對於原始返回類型),對於指望引用類型的返回類型將轉換爲 null
  • 不管靜態類型如何,都假定空值是正確的,而且能夠經過

讓咱們看一個考慮如下規則的簡單調用示例:

Object rcvr = "a";
try {
    MethodType mt = MethodType.methodType(int.class);
    MethodHandles.Lookup l = MethodHandles.lookup();
    MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt);

    int ret;
    try {
        ret = (int)mh.invoke(rcvr);
        System.out.println(ret);
    } catch (Throwable t) {
        t.printStackTrace();
    }
} catch (IllegalArgumentException | NoSuchMethodException | SecurityException e) {
    e.printStackTrace();
} catch (IllegalAccessException x) {
    x.printStackTrace();
}

在更復雜的示例中,方法句柄能夠提供一種更清晰的方法來執行與核心反射相同的動態編程任務。不只如此,並且方法句柄從一開始就被設計爲能夠更好地與JVM的低級執行模型一塊兒使用,而且可能提供更好的性能(儘管性能故事還在不斷髮展)。

方法處理和調用動態

invokedynamic 經過引導方法機制使用方法句柄。與 invokevirtual 不一樣,invokedynamic 指令沒有接收器對象。相反,它們的行爲相似於 invokestatic,並使用 BSM 返回 CallSite 類型的對象。該對象包含一個方法句柄(稱爲「目標」),該句柄表示將做爲 invokedynamic 指令的結果執行的方法。

當加載包含 invokedynamic 的類時,調用點被稱爲處於「非限制」狀態,而且在 BSM 返回以後,聽說生成的CallSite和方法句柄被「限制」到了調用站點中。

BSM的簽名以下所示(請注意,BSM能夠具備任何名稱):

static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type);

若是要建立實際上包含 invokedynamic 的代碼,則須要使用字節碼操做庫(由於Java語言不包含所需的構造)。在本文的其他部分,咱們將須要使用ASM庫來生成包含 invokedynamic 指令的字節碼。從 Java 應用程序的角度來看,這些文件顯示爲常規的類文件(儘管它們固然沒有 Java 源代碼表示形式)。Java 代碼將它們視爲「黑匣子」,儘管如此,咱們仍然能夠調用方法並利用 invokedynamic 和相關功能。

讓咱們看一下一個基於ASM的類,該類使用invokedynamic建立一個「 Hello World」。

public class InvokeDynamicCreator {

    public static void main(final String[] args) throws Exception {
        final String outputClassName = "kathik/Dynamic";
        try (FileOutputStream fos
                = new FileOutputStream(new File("target/classes/" + outputClassName + ".class"))) {
            fos.write(dump(outputClassName, "bootstrap", "()V"));
        }
    }

    public static byte[] dump(String outputClassName, String bsmName, String targetMethodDescriptor)
            throws Exception {
        final ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        // Setup the basic metadata for the bootstrap class
        cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, outputClassName, null, "java/lang/Object", null);

        // Create a standard void constructor
        mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        // Create a standard main method
        mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        mv.visitCode();
        MethodType mt = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class,
                MethodType.class);
        Handle bootstrap = new Handle(Opcodes.H_INVOKESTATIC, "kathik/InvokeDynamicCreator", bsmName,
                mt.toMethodDescriptorString());
        mv.visitInvokeDynamicInsn("runDynamic", targetMethodDescriptor, bootstrap);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 1);
        mv.visitEnd();

        cw.visitEnd();

        return cw.toByteArray();
    }

    private static void targetMethod() {
        System.out.println("Hello World!");
    }

    public static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
        final MethodHandles.Lookup lookup = MethodHandles.lookup();
        // Need to use lookupClass() as this method is static
        final Class<?> currentClass = lookup.lookupClass();
        final MethodType targetSignature = MethodType.methodType(void.class);
        final MethodHandle targetMH = lookup.findStatic(currentClass, "targetMethod", targetSignature);
        return new ConstantCallSite(targetMH.asType(type));
    }
}

該代碼分爲兩部分,第一部分使用 ASM Visitor API 建立一個名爲 kathik.Dynamic 的類文件。請注意對visitInvokeDynamicInsn() 的鍵調用。第二部分包含將綁定到調用點中的目標方法,以及 invokedynamic 指令所需的BSM。

請注意,這些方法在 InvokeDynamicCreator 類以內,而不是咱們生成的類 kathik.Dynamic 的一部分。這意味着在運行時,InvokeDynamicCreator 也必須位於類路徑以及 kathik.Dynamic 上,不然將沒法找到該方法。

運行 InvokeDynamicCreator 時,它將建立一個新的類文件 Dynamic.class,其中包含一個 invokedynamic 指令,如咱們在類上使用javap所看到的:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokedynamic #20,  0             // InvokeDynamic #0:runDynamic:()V
         5: return

這個例子展現了最簡單的 invokedynamic 狀況,它使用了常量 CallSite 對象的特殊狀況。這意味着BSM(和查找)僅執行一次,所以後續調用很快。

可是,更復雜的 invokedynamic 用法會很快變得複雜,尤爲是在程序的生命週期中調用點目標方法能夠更改時。

在下一篇文章中,咱們將研究一些更高級的用例並構建一些示例,並更深刻地研究invokedynamic的細節。

相關文章
相關標籤/搜索