什么是 invokedDynamic 以及如何使用它?

我一直听说有很多新的很酷的特性被添加到 JVM 中,其中一个很酷的特性就是 invokedDynamic。我想知道它是什么,它是如何使 Java 中的反射编程更容易或更好的?

56394 次浏览

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

不久前,C # 增加了一个很酷的特性,C # 中的动态语法

Object obj = ...; // no static type available
dynamic duck = obj;
duck.quack(); // or any method. no compiler checking.

可以把它看作是反射方法调用的语法糖。它可以有非常有趣的应用程序。参见 http://www.infoq.com/presentations/Statically-Dynamic-Typing-Neal-Gafter

Neal Gafter,负责 C # 的动态类型,刚刚从 SUN 叛变到 MS。所以认为 SUN 内部讨论过同样的事情并不是不合理的。

我记得在那之后不久,一些 Java 的家伙宣布了类似的东西

InvokeDynamic duck = obj;
duck.quack();

不幸的是,在 Java7中找不到这个特性。非常失望。对于 Java 程序员来说,他们没有简单的方法在他们的程序中利用 invokedynamic

在继续调用 Dynamic 之前,有两个概念需要理解。

1. 静态与动态类型

静态 -预先在编译时进行类型检查(例如 Java)

Dynamic -预先在运行时进行类型检查(例如 JavaScript)

类型检查是一个验证程序是否为类型安全的过程,即检查类和实例变量、方法参数、返回值和其他变量的类型信息。 Java 知道 int,String,。.而 JavaScript 中对象的类型只能在运行时确定

2. 强打字与弱打字

Strong -指定对提供给其操作(例如 Java)的值类型的限制

-如果操作的参数具有不兼容的类型(例如 VisualBasic) ,则转换(强制转换)这些参数

既然 Java 是静态和弱类型的,那么如何在 JVM 上实现动态和强类型语言呢?

InvokedDynamic 实现了一个运行时系统,该系统可以选择方法或函数的最合适的实现ーー在编译程序之后。

使用(a + b)并且在编译时对变量 a,b 一无所知,invokedDynamic 在运行时将这个操作映射到 Java 中最合适的方法。例如,如果 a,b 是字符串,那么调用方法(字符串 a,字符串 b)。如果 a,b 是 int,那么调用 method (int a,int b)。

InvokedDynamic 是用 Java7引入的。

作为我的 Java 唱片公司文章的一部分,我阐述了 InvokeDynamic 背后的动机。让我们从印第的粗略定义开始。

介绍一下 Indy

调用 Dynamic (也称为 印第)是 < a href = “ https://jcp.org/en/JSR/Details? id = 292”rel = “ noReferrer”> JSR 292 的一部分,目的是增强对 Dynamic Type Language 的 JVM 支持。在 Java7中首次发布之后,invokedynamic操作码及其 java.lang.invoke包被基于 JVM 的动态语言(如 JRuby)广泛使用。

尽管 indy 专门设计来增强动态语言支持,但它提供的远不止这些。事实上,是 它适合用在语言设计者需要任何形式的动态性的地方,从动态类型的杂技到动态策略!

例如,爪哇8 Lambda 表达式实际上是使用 invokedynamic实现的,尽管 Java 是静态类型语言!

用户-可定义字节码

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

尽管存在差异,但这些调用类型有一个共同的特征: 我们不能用自己的逻辑来丰富它们

Indy 是怎么工作的?

当 JVM 第一次看到 invokedynamic指令时,它调用一个名为 自助法的特殊静态方法。这个自助法是一段 Java 代码,我们编写这段代码是为了准备实际的被调用逻辑:

enter image description here

然后自助法返回一个 java.lang.invoke.CallSite的实例,这个 CallSite保存着对实际方法的引用,也就是 MethodHandle

从现在开始,每当 JVM 再次看到这个 invokedynamic指令时,它都会跳过 慢路,直接调用底层的可执行文件。JVM 继续跳过缓慢的路径,除非有什么变化。

示例: Java14记录

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

考虑到这个简单的记录:

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 的 鞋带方法称为 bootstrap,它驻留在 java.lang.runtime.ObjectMethods类中。正如你所看到的,这个自助法需要以下参数:

  • 表示查找上下文的 MethodHandles.Lookup实例 (Ljava/lang/invoke/MethodHandles$Lookup部分)。
  • 该方法命名为引导程序(例如 toStringequalshashCode等) 例如,当值为 toString时,引导 将返回一个 ConstantCallSite(一个从不改变的 CallSite) 指向此特定的实际 toString实现 录音。
  • 方法的 TypeDescriptor(Ljava/lang/invoke/TypeDescriptor 部分)。
  • 一个类型标记,即 Class<?>,表示 Record 类类型 在这种情况下 Class<Range>
  • 所有组件名称的分号分隔列表,即 min;max
  • 每个成分一个 abc 0这样自助法就可以 根据这个特定的组件创建一个 MethodHandle 方法的实施。

invokedynamic指令将所有这些参数传递给自助法。自助法依次返回一个 ConstantCallSite的实例。这个 ConstantCallSite持有对请求的方法实现的引用,例如 toString

为什么是 Indy?

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

除了效率的论据,invokedynamic方法更可靠,因为 它的简单的脆性较小。

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

最后,让我们假设一个新版本的 Java 包含一个新的、更有效的自助法实现。使用 invokedynamic,我们的应用程序可以在不重新编译的情况下利用这一改进。这样我们就有了某种 前向二进制兼容性。而且,这就是我们所说的动态策略!

其他例子

除了 Java Records,调用 Dynamic还被用来实现以下特性:

简短的回答是 invokedDynamic 是 JVM 中的一个新操作码,在 JAVA7之前不存在。

至于反射,在这个定义的上下文中: Java 反射是在运行时检查或修改类的运行时行为的过程。,但是,我认为需要更多的解释。 从 文章以下:

例如,反射早于集合和泛型 结果,方法签名由反射中的 Class []表示 这可能会非常麻烦和容易出错,并且会受到 Java 的数组语法冗长的性质 需要手动装箱和取消装箱的基本类型,并围绕 无效方法的可能性。

方法句柄

而不是强迫程序员处理 针对这些问题,Java7引入了一个新的 API,称为 MethodHandles, 表示必要的抽象。这个 API 的核心是 调用,特别是类 MethodHandle。 此类型的实例提供调用方法的能力,并且它们 是直接可执行的 参数和返回类型,它提供的类型安全性与 考虑到使用它们的动态方式,这些 API 是可能的 也可以单独使用,在这种情况下 它可以被认为是现代的、安全的反思替代品

引自 使用 invokedDynamic 理解 Java 方法调用

这四个字节码是 在 Java8和 Java9中使用的方法调用,它们是 Invokevirtal、 invokspecial、 invokeinterface 和 invokestatic

这就提出了一个问题,第五个操作码,invokedDynamic, 简短的回答是,在 Java9中,有 Java 语言中没有对 invokedDynamic 的直接支持。

实际上,当在 Java7中将 invokedDynamic 添加到运行时时, Javac 编译器在任何情况下都不会发出新的字节码 无论如何。

从 Java8开始,invokedDynamic 被用作主要的实现 机制,以提供先进的平台功能 这种操作码使用的最简单的例子在 Lambda 表达式的实现。

因此,invokedDynamic 是一个新的操作码,它允许在 JAVA 中使用一个新的对象引用类型,即 Lambda。