invokedynamic_Invokedynamic-Java的秘密武器

invokedynamic

Java 7的发行版包含了一些新功能,乍一看似乎对Java开发人员来说用途有限,我们在本文中介绍了其中的一些功能。

但是,事实证明,一项特定功能对于实现Java 8的“标题”功能(例如lambda和默认方法)至关重要。 在本文中,我们想深入研究invokedynamic,并解释为什么它对于Java平台以及JVM语言(如JRuby和Nashorn)如此强大。

最早关于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的两种可能的用例。 此操作码用于应在运行时准确确定调度的情况下,尤其是既不希望也不可能进行方法覆盖的情况。 该示例演示的两种情况是私有方法超级调用 ,因为此类方法在编译时是已知的,并且不能被覆盖。

精明的读者会注意到,所有对Java方法的调用都被编译为这四个操作码之一,因此出现了问题-invokedynamic的作用是什么,为什么对Java开发人员有用?

这些功能的主要目标是创建一个字节码来处理一种新的方法分派-本质上,它允许应用程序级代码确定调用将执行的方法,并且仅在调用即将执行时才这样做。 与以前提供的Java平台相比,这使得语言和框架编写者可以支持更多的动态编程样式。

目的是使用户代码使用方法处理API来确定运行时的调度,而不会遭受性能损失和与反射相关的安全问题。 实际上,一旦功能充分成熟,invokedynamic的既定目标将与常规方法调度(调用虚拟)一样快。

当Java 7到来时,JVM支持执行新的字节码,但是无论提交的Java代码如何,javac都不会产生包含invokedynamic的字节码。 相反,该功能用于支持在JVM上运行的JRuby和其他动态语言。

这在Java 8中发生了变化,在Java 8中,现在已经生成了invokedynamic,并在后台使用了lambda表达式和默认方法以及Nashorn的主要调度机制。 但是,Java应用程序开发人员仍然没有直接方法来进行完全动态的方法解析。 也就是说,Java语言没有可创建通用的invokedynamic调用站点的关键字或库。 这意味着尽管它提供了强大的功能,但是对于大多数Java开发人员来说,该机制仍然晦涩难懂。 让我们看看如何在我们的代码中利用它。

方法句柄简介

为了使invokedynamic正常工作,关键概念是方法句柄。 这是表示应该从invokedynamic调用站点调用的方法的一种方式。 一般的想法是,每个invokedynamic指令都与一个特殊的方法(称为引导方法或BSM)相关联。 当解释器到达invokedynamic指令时,将调用BSM。 它返回一个对象(包含方法句柄),该对象指示调用站点应实际执行的方法。

这有点类似于反射,但是反射具有局限性,使其不适合与invokedynamic一起使用。 相反,将java.lang.invoke.MethodHandle(和子类)添加到Java 7 API中,以表示invokedynamic可以定位的方法。 MethodHandle类从JVM接受了一些特殊待遇,以便正确运行。

思考方法句柄的一种方法是以一种安全,现代的方式完成核心反射,并尽可能实现类型安全。 它们对于invokedynamic是必需的,但也可以独立使用。

方法类型

Java方法可以认为由四个基本部分组成:

  • 名称
  • 签名(包括返回类型)
  • 定义的类别
  • 实现该方法的字节码

这意味着,如果要引用方法,则需要一种有效地表示方法签名的方法(而不是使用必须使用反射的可怕的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的细节。

翻译自: https://www.infoq.com/articles/Invokedynamic-Javas-secret-weapon/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

invokedynamic

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值