为什么使用 invokedynamic 调用 Java 8 lambdas?
该指令用于帮助 VM 在运行时确定方法引用,而不是在编译时硬连接方法。invokedynamic
这对于动态语言非常有用,在这些语言中,确切的方法和参数类型直到运行时才知道。但Java lambdas的情况并非如此。它们被转换为具有明确定义的参数的静态方法。可以使用 调用此方法。invokestatic
那么,lambdas的需求是什么,特别是当性能受到冲击时?invokedynamic
该指令用于帮助 VM 在运行时确定方法引用,而不是在编译时硬连接方法。invokedynamic
这对于动态语言非常有用,在这些语言中,确切的方法和参数类型直到运行时才知道。但Java lambdas的情况并非如此。它们被转换为具有明确定义的参数的静态方法。可以使用 调用此方法。invokestatic
那么,lambdas的需求是什么,特别是当性能受到冲击时?invokedynamic
Lambda 不是使用 调用的,它们的对象表示是使用 创建的,实际的调用是正则或 。invokedynamic
invokedynamic
invokevirtual
invokeinterface
例如:
// creates an instance of (a subclass of) Consumer
// with invokedynamic to java.lang.invoke.LambdaMetafactory
something(x -> System.out.println(x));
void something(Consumer<String> consumer) {
// invokeinterface
consumer.accept("hello");
}
任何 lambda 都必须成为某个基类或接口的实例。该实例有时将包含从原始方法捕获的变量的副本,有时包含指向父对象的指针。这可以作为匿名类实现。
为什么要调用动态
简短的答案是:在运行时生成代码。
Java维护者选择在运行时生成实现类。这是通过调用 来完成的。由于该调用的参数(返回类型、接口和捕获的参数)可以更改,因此这需要 。java.lang.invoke.LambdaMetafactory.metafactory
invokedynamic
用于在运行时构造匿名类,允许 JVM 在运行时生成该类字节码。对同一语句的后续调用使用高速缓存版本。使用的另一个原因是将来能够更改实现策略,而不必更改已编译的代码。invokedynamic
invokedynamic
未走的路
另一个选项是编译器为每个 lambda 实例化创建一个内部类,等效于将上述代码转换为:
something(new Consumer() {
public void accept(x) {
// call to a generated method in the base class
ImplementingClass.this.lambda$1(x);
// or repeating the code (awful as it would require generating accesors):
System.out.println(x);
}
);
这需要在编译时创建类,并且必须在运行时加载。jvm 的工作方式是,这些类将与原始类位于同一目录中。第一次执行使用该 lambda 的语句时,必须加载并初始化该匿名类。
关于性能
第一次调用 将触发匿名类生成。然后,操作码被替换为在性能上与手动写入匿名实例化等效的代码。invokedynamic
invokedynamic
Brain Goetz在他的一篇论文中解释了lambda翻译策略的原因,不幸的是,现在似乎无法获得。幸运的是,我保留了一份副本:
翻译策略
我们可以通过多种方式在字节码中表示 lambda 表达式,例如内部类、方法句柄、动态代理等。这些方法中的每一种都有优点和缺点。在选择策略时,有两个相互竞争的目标:通过不承诺特定策略来最大化未来优化的灵活性,以及在类文件表示中提供稳定性。我们可以通过使用 JSR 292 中的 invokedynamic 特性来实现这两个目标,将字节码中创建 lambda 的二进制表示形式与在运行时计算 lambda 表达式的机制分开。我们不是生成字节码来创建实现 lambda 表达式的对象(例如调用内部类的构造函数),而是描述构造 lambda 的配方,并将实际构造委托给语言运行时。该配方被编码在调用动态指令的静态和动态参数列表中。
使用 invokedynamic 可以让我们将转换策略的选择推迟到运行时。运行时实现可以自由动态选择策略来评估 lambda 表达式。运行时实现选择隐藏在用于lambda构造的标准化(即平台规范的一部分)API后面,因此静态编译器可以对此API发出调用,JRE实现可以选择自己喜欢的实现策略。调用动力学机制允许在没有这种后期绑定方法可能带来的性能成本的情况下完成此操作。
当编译器遇到 lambda 表达式时,它首先将 lambda 主体降低(desugars)到一个方法中,该方法的参数列表和返回类型与 lambda 表达式的参数列表和返回类型匹配,可能还有一些其他参数(对于从词法范围捕获的值,如果有)。在捕获 lambda 表达式的位置,它会生成一个 invokedynamic 调用站点,当调用该站点时,将返回 lambda 要转换为的功能接口的实例。此调用站点称为给定 lambda 的 lambda 工厂。lambda 工厂的动态参数是从词法范围捕获的值。lambda 工厂的 bootstrap 方法是 Java 语言运行时库中的标准化方法,称为 lambda 元工厂。静态引导参数在编译时捕获有关 lambda 的已知信息(它将被转换为的功能接口、解糖 lambda 主体的方法句柄、有关 SAM 类型是否可序列化的信息等)。
方法引用的处理方式与 lambda 表达式相同,只是大多数方法引用不需要解砂到新方法中;我们可以简单地为引用的方法加载一个常量方法句柄,并将其传递给元工厂。
因此,这里的想法似乎是封装翻译策略,而不是通过隐藏这些细节来致力于特定的做事方式。将来,当类型擦除和缺少值类型的问题得到解决并且Java支持实际的函数类型时,它们也可以去那里为另一个函数类型更改该策略,而不会在用户的代码中引起任何问题。