什么是调用动态,我如何使用它?介绍印地 用户可定义的字节码 印地如何工作? 示例:Java 14 记录 为什么选择印地? 其他示例

2022-08-31 06:52:59

我一直听说所有新的很酷的功能正在添加到JVM中,其中一个很酷的功能是调用动力学。我想知道它是什么,它如何使Java中的反思性编程更容易或更好?


答案 1

这是一个新的JVM指令,它允许编译器生成代码,这些代码调用比以前更宽松的规范的方法 - 如果你知道“鸭子类型”是什么,invokedynamic基本上允许鸭子类型化。作为Java程序员,你可以用它做很多事情。但是,如果您是工具创建者,则可以使用它来构建更灵活,更高效的基于JVM的语言。这是一篇非常甜蜜的博客文章,提供了很多细节。


答案 2

作为我的Java Records文章的一部分,我阐述了调用动态背后的动机。让我们从Indy的粗略定义开始。

介绍印地

Invoke Dynamic(也称为Indy)是JSR 292的一部分,旨在增强对Dynamic Type Languages的JVM支持。在Java 7中首次发布后,操作码及其行李箱被基于JVM的动态语言(如JRuby)广泛使用。invokedynamicjava.lang.invoke

虽然indy专门设计用于增强动态语言支持,但它提供的远不止于此。事实上,它适用于语言设计师需要任何形式的动态性的任何地方,从动态类型杂技到动态策略!

例如,Java 8 Lambda 表达式实际上是使用 实现的,即使 Java 是一种静态类型语言!invokedynamic

用户可定义的字节码

很长一段时间以来,JVM 确实支持四种方法调用类型:调用静态方法、调用接口方法、调用构造函数或私有方法和调用实例方法。invokestaticinvokeinterfaceinvokespecialsuper()invokevirtual

尽管它们存在差异,但这些调用类型有一个共同的特征:我们不能用自己的逻辑来丰富它们。相反,使我们能够以任何我们想要的方式引导调用过程。然后,JVM 负责直接调用 Bootstrapped 方法。invokedynamic

印地如何工作?

JVM 第一次看到指令时,它会调用一个名为 Bootstrap Method 的特殊静态方法。bootstrap 方法是我们编写的一段 Java 代码,用于准备实际的待调用逻辑:invokedynamic

enter image description here

然后,引导方法返回 的实例。这包含对实际方法的引用,即 .java.lang.invoke.CallSiteCallSiteMethodHandle

从现在开始,每次 JVM 再次看到此指令时,它都会跳过慢速路径并直接调用底层可执行文件。JVM 将继续跳过慢速路径,除非发生任何更改。invokedynamic

示例:Java 14 记录

Java 14提供了一个很好的紧凑语法来声明应该是哑数据持有者的类。Records

考虑这个简单的记录:

public record Range(int min, int max) {}

此示例的字节码如下所示:

Compiled from "Range.java"
public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokedynamic #18,  0 // InvokeDynamic #0:toString:(LRange;)Ljava/lang/String;
         6: areturn

在其引导方法表中

BootstrapMethods:
  0: #41 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:
     (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
     Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;
     Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
    Method arguments:
      #8 Range
      #48 min;max
      #50 REF_getField Range.min:I
      #51 REF_getField Range.max:I

因此,调用 Records 的引导方法,该方法驻留在类中。如您所见,此引导方法需要以下参数:bootstrapjava.lang.runtime.ObjectMethods

  • 表示查找上下文(部件)的实例。MethodHandles.LookupLjava/lang/invoke/MethodHandles$Lookup
  • 引导程序将要链接的方法名称(即 、 、 等)。例如,当值为 时,bootstrap 将返回一个(永不更改的)指向此特定记录的实际实现。toStringequalshashCodetoStringConstantCallSiteCallSitetoString
  • for 方法 ( 部分)。TypeDescriptorLjava/lang/invoke/TypeDescriptor
  • 类型标记,即,表示记录类类型。就是在这种情况下。Class<?>Class<Range>
  • 所有组件名称的分号分隔列表,即 .min;max
  • 每个组件一个。这样,bootstrap 方法就可以基于此特定方法实现的组件创建。MethodHandleMethodHandle

该指令将所有这些参数传递给引导方法。反过来,Bootstrap 方法返回 的实例。这是对所请求方法实现的引用,例如。invokedynamicConstantCallSiteConstantCallSitetoString

为什么选择印地?

与反射 API 相反,API 非常高效,因为 JVM 可以完全看穿所有调用。因此,JVM可以应用各种优化,只要我们尽可能避免慢速路径!java.lang.invoke

除了效率论证之外,由于其简单性,该方法更可靠,更不脆弱。invokedynamic

此外,为 Java 记录生成的字节码与属性数无关。因此,更少的字节码和更快的启动时间。

最后,假设新版本的Java包含一个新的,更有效的引导方法实现。使用 ,我们的应用程序可以利用此改进,而无需重新编译。通过这种方式,我们有某种前向二进制兼容性。此外,这就是我们正在谈论的动态策略!invokedynamic

其他示例

除了 Java 记录之外,调用动态还用于实现以下功能: