Invokedynamic-Java的秘密武器

Java基础

浏览数:76

2019-10-14

AD:资源代下载服务

最早关于invokedynamic的工作至少可以追溯到2007年,首次成功进行的动态调用是在2008年8月26日进行的。这早于Sun被Oracle收购之前,并且按照大多数开发人员的标准,该功能已经开发了很长时间。 。

invokedynamic的卓越之处在于它是自Java 1.0以后的第一个新增的字节码。它加入了现有的调用字节码invokevirtual,invokestatic,invokeinterface和invokespecial。这四个现有操作码实现了Java开发人员通常熟悉的所有形式的方法分派,特别是:

  • invokevirtual -实例方法的标准调用
  • invokestatic -用于分派静态方法
  • invokeinterface -用于通过接口调用方法
  • invokespecial -在需要非虚拟(即“精确”)调度时使用

一些开发人员可能对平台为何需要全部四个操作码感到好奇,所以让我们看一个使用不同的调用操作码的简单示例,以说明它们之间的区别:

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 方法的位置为止。

相反,该调用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的细节。

作者:Yujiaao