Invokedynamic——Java的秘密武器

EveFlynn 7年前發布 | 17K 次閱讀 Java Java開發

在Java 7的發布版中包含了多項新的特性,這些特性乍看上去Java開發人員對它們的使用非常有限,在我們之前的文章中,曾經對其進行過介紹。

但是,其中有項特性對于實現Java 8中“頭版標題”類型的特性來說至關重要(如lambdas和默認方法)。在本文中,我們將會深入學習invokedynamic,并闡述它對于Java平臺以及像JRuby和Nashorn這樣的JVM語言來講為何如此重要。

invokedynamic最初的工作 至少始于2007年,而第一次成功的動態調用發生在2008年8月26日。這比Oracle收購Sun還要早,按照大多數開發人員的標準,這個特性的研發已經持續了相當長的時間。

值得注意的是,從Java 1.0到現在,invokedynamic是第一個新加入的Java字節碼,它與已有的字節碼invokevirtual、invokestatic、invokeinterface和invokespecial組合在了一起。已有的這四個操作碼實現了Java開發人員所熟知的所有形式的方法分派(dispatch):

  • invokevirtual——對實例方法的標準分派
  • invokestatic——用于分派靜態方法
  • invokeinterface——用于通過接口進行方法調用的分派
  • invokespecial——當需要進行非虛(也就是“精確”)分派時會用到

有些開發人員可能會好奇平臺為何需要這四種操作碼,所以我們看一個簡單的樣例,這個樣例會用到不同的調用操作碼,以此來闡述它們之間的差異:

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

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

        ArrayList 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."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class kathik/InvokeExamples
       3: dup
       4: invokespecial #3                  // Method "":()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."":()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."":()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是一個接口。所以,在運行時方法表(通常稱為“vtable”)中,add()方法的精確位置還沒有在編譯時確定。因此,源碼編譯器會生成一個invokeinterface指令,將實際的方法查找推遲到運行期,也就是當ls的實際vtable能夠探查到并且add()方法的位置能夠找到的時候。

與之相反,對 als.add("Dydh Da") 的調用是通過als來執行的,這里的靜態類型是類類型(class type)—— ArrayList<String> 。這意味著在vtable中,方法的位置在編譯期是可知的。因此,javac會針對這個精確的vtable條目生成一個invokevirtual指令。不過,最終的方法選擇依然是在運行期確定的,因為這里還有方法重寫(overriding)的可能性,但是vtable slot在編譯期就已經確定了。

除此之外,這個樣例還展現了invokespecial的兩個使用場景。這個操作碼用于在運行時確定如何分派的場景之中,具體來講,在這里沒有方法重寫的需求,另外這也不可能實現。樣例中所闡述的場景是 private methodssuper calls ,這些方法在編譯期是可知的,并且無法進行重寫。

細心的讀者可能已經發現,對Java方法的所有調用都編譯成了四個操作碼中的某一個,那么問題就來了——invokedynamic是做什么的,它對于Java開發人員有什么用處呢?

這個特性的主要目標在于創建一個字節碼,用于處理新型的方法分派——它的本質是允許應用級別的代碼來確定執行哪一個方法調用,只有在調用要執行的時候,才會進行這種判斷。這樣的話,相對于Java平臺之前所提供的編程風格,允許語言和框架的編寫人員支持更加動態的編碼風格。

它的目的在于由用戶代碼通過方法句柄API(method handles API)在運行時確定如何分派,同時避免反射帶來的性能懲罰和安全問題。實際上,invokedynamic所宣稱的目標就是一旦該特性足夠成熟,它的速度要像常規的方法分派(invokevirtual)一樣快。

當Java 7發布的時候,JVM就已經支持執行新的字節碼了,但是不管提交什么樣的Java代碼,javac都不會產生包含invokedynamic的字節碼。這項特性用來支持JRuby和其他運行在JVM上的動態語言。

在Java 8中,這發生了變化,在實現lambda表達式和默認方法時,底層會生成和使用invokedynamic,它同時還會作為Nashorn的首選分派機制。但是,對于Java應用的開發人員來說,依然沒有直接的方式實現完全的動態方法處理(resolution)。也就是說,Java語言并沒有提供關鍵字或庫來創建通用的invokedynamic調用點(call site)。這意味著,盡管這種機制的功能非常強大,但它對于大多數的Java開發人員來說依然有些陌生。接下來,我們看一下如何在自己的代碼中使用這項技術。

方法句柄簡介

要讓invokedynamic正常運行,一個核心的概念就是方法句柄(method handle)。它代表了一個可以從invokedynamic調用點進行調用的方法。這里的基本理念就是每個invokedynamic指令都會與一個特定的方法關聯(也就是引導方法或BSM)。當解釋器(interpreter)遇到invokedynamic指令的時候,BSM會被調用。它會返回一個對象(包含了一個方法句柄),這個對象表明了調用點要實際執行哪個方法。

在一定程度上,這與反射有些類似,但是反射有它的局限性,這些局限性使它不適合與invokedynamic協作使用。Java 7 API中加入了java.lang.invoke.MethodHandle(及其子類),通過它們來代表invokedynamic指向的方法。為了實現操作的正確性,MethodHandle會得到JVM的一些特殊處理。

理解方法句柄的一種方式就是將其視為以安全、現代的方式來實現反射的核心功能,在這個過程會盡可能地保證類型的安全。invokedynamic需要方法句柄,另外它們也可以單獨使用。

方法類型

一個Java方法可以視為由四個基本內容所構成:

  • 名稱
  • 簽名(包含返回類型)
  • 定義它的類
  • 實現方法的字節碼

這意味著如果要引用某個方法,我們需要有一種有效的方式來表示方法簽名(而不是反射中強制使用的令人討厭的Class<?>[] hack方式)。

接下來我們采用另外的方式,方法句柄首先需要的一個構建塊就是表達方法簽名的方式,以便于查找。在Java 7引入的Method Handles API中,這個角色是由java.lang.invoke.MethodType類來完成的,它使用一個不可變的實例來代表簽名。要獲取MethodType,我們可以使用methodType()工廠方法。這是一個參數可變(variadic)的方法,以class對象作為參數。

第一個參數所使用的class對象,對應著簽名的返回類型;剩余參數中所使用的class對象,對應著簽名中方法參數的類型。例如:

//toString()的簽名
MethodType mtToString = MethodType.methodType(String.class);

// setter方法的簽名
MethodType mtSetter = MethodType.methodType(void.class, Object.class);

// Comparator中compare()方法的簽名
MethodType mtStringComparator = MethodType.methodType(int.class, String.class, String.class);

現在我們就可以使用MethodType,再組合方法名稱以及定義方法的類來查找方法句柄。要實現這一點,我們需要調用靜態的MethodHandles.lookup()方法。這樣的話,會給我們一個“查找上下文(lookup context)”,這個上下文基于當前正在執行的方法(也就是調用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()。這兩個方法都是以接收者(receiver)和調用變量作為參數,所以它們的簽名為:

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

兩者的區別在于,invokeExact()在調用方法句柄時會試圖嚴格地直接匹配所提供的變量。而invoke()與之不同,在需要的時候,invoke()能夠稍微調整一下方法的變量。invoke()會執行一個asType()轉換,它會根據如下的這組規則來進行變量的轉換:

  • 如果需要的話,原始類型會進行裝箱操作
  • 如果需要的話,裝箱后的原始類型會進行拆箱操作
  • 如果必要的話,原始類型會進行擴展
  • void返回類型會轉換為0(對于返回原始類型的情況),而對于預期得到引用類型的返回值的地方,將會轉換為null
  • 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

invokedynamic指令通過引導方法(bootstrap method,BSM)機制來使用方法句柄。與invokevirtual指令不同,invokedynamic指令沒有接收者對象。相反,它們的行為類似于invokestatic,會使用BSM來返回一個CallSite類型的對象。這個對象包含一個方法句柄(稱之為“target”),它代表了當前invokedynamic指令要執行的方法。

當包含invokedynamic的類加載時,調用點會處于“unlaced”狀態,在BSM返回之后,得到的CallSite和方法句柄會讓調用點處于“laced”狀態。

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;

        // 為引導類搭建基本的元數據
        cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, outputClassName, null, "java/lang/Object", null);

        // 創建標準的void構造器
        mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V");
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        // 創建標準的main方法
        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();
        // 需要使用lookupClass(),因為這個方法是靜態的
        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(和lookup)只會執行一次,所以后續的調用會很快。

但是,針對invokedynamic的高級用法很快就會變得非常復雜,當調用點和目標方法在程序生命周期中會發生變化時更是如此。

在后續的文章中,我們將會探討一些高級的使用場景并構建一些樣例,深入研究invokedynamic的細節。

 

 

來自:http://www.infoq.com/cn/articles/Invokedynamic-Javas-secret-weapon

 

 本文由用戶 EveFlynn 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!