为什么 Java8 lambdas 是使用 invokedDynamic 调用的?

invokedynamic指令用于帮助 VM 在运行时确定方法引用,而不是在编译时硬连接它。

这对于动态语言非常有用,因为确切的方法和参数类型直到运行时才知道。但 Java lambdas 的情况并非如此。它们被转换为具有定义良好的参数的静态方法。可以使用 invokestatic调用此方法。

那么,invokedynamic对 lambdas 的需求是什么,特别是在性能受到影响的情况下?

18371 次浏览

Lambdas 不是使用 invokedynamic调用的,它们的对象表示是使用 invokedynamic创建的,实际调用是常规的 invokevirtualinvokeinterface

例如:

// 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 都必须成为某个基类或接口的实例。该实例有时包含从原始方法捕获的变量的副本,有时包含指向父对象的指针。 这可以作为一个匿名类实现。

为什么要调用 Dynamic

简短的回答是: 在运行时生成代码。

Java 维护者选择在运行时生成实现类。 这是通过调用 java.lang.invoke.LambdaMetafactory.metafactory完成的。 由于该调用的参数(返回类型、接口和捕获的参数)可以更改,因此需要 invokedynamic

使用 invokedynamic在运行时构造匿名类,允许 JVM 在运行时生成该类的字节码。对同一语句的后续调用使用缓存版本。使用 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 表达式 字节码,例如内部类、方法句柄、动态代理和 这些方法各有利弊 战略,有两个相互竞争的目标: 最大限度地灵活 未来的优化,通过不承诺一个具体的战略,vs 提供类文件表示的稳定性。我们可以实现 通过使用从 JSR 292到 在字节码中分离 lambda 创建的二进制表示 从在运行时计算 lambda 表达式的机制。 而不是生成字节码来创建实现 Lambda 表达式(例如调用内部的构造函数) 类) ,我们描述了构造 lambda 的方法,并委托 语言运行库的实际构造 编码在 invokedDynamic 的静态和动态参数列表中 指示。

使用 invokedDynamic 可以让我们推迟翻译的选择 策略,直到运行时。运行时实现可以自由选择 动态计算 lambda 表达式的策略 实现选择隐藏在标准化(即,部分 (平台规范)用于 lambda 构造的 API,因此 静态编译器可以发出对这个 API 和 JRE 实现的调用 可以选择他们首选的实现策略 机制允许这样做没有性能成本 否则,这种后期约束方法可能会造成。

当编译器遇到 lambda 表达式时,它首先降低 (desugars)将 lambda 体转换为一个方法,该方法的参数列表和 返回类型与 lambda 表达式的返回类型匹配 附加参数(对于从词法范围捕获的值,如果 在 lambda 表达式被捕获的时候, 它生成一个 invokedDynamic 调用站点,当调用该站点时,该站点将返回 Lambda 所在的函数接口的实例 这个调用站点被称为给定的 lambda 工厂 Lambda 工厂的动态参数是值 从词汇范围捕捉到的 lambda 的自助法 Factory 是 Java 语言运行时库中的标准化方法, 称为 lambda 元工厂。静态引导参数捕获 在编译时已知的关于 lambda 的信息(函数 接口的方法句柄,该接口将被转换为 Desugared lambda 体,关于 SAM 类型是否为 可序列化等)

方法引用的处理方式与 lambda 表达式相同, 除了大多数方法引用不需要设计成 新方法; 我们可以简单地加载一个常量方法句柄 方法,并将其传递给元工厂。

因此,这里的想法似乎是封装翻译策略,而不是通过隐藏这些细节来承诺一种特定的做事方式。在未来,当类型擦除和缺少值类型的问题得到解决,而且 Java 可能支持实际的函数类型时,他们可能会去那里,改变另一个策略,而不会在用户代码中造成任何问题。

当前 Java8的 lambda 实现是一个复合决策:

    1. 将 lambda 表达式编译为封闭类中的一个静态方法; 而不是将 lambda 编译为单独的内部类文件(Scala 以这种方式编译,生成许多 $$class 文件)
    1. 引入一个常量池: BootstrapMethods,它将静态方法调用封装到 Callsite 对象(可以缓存以供以后使用)

回答你的问题,

    1. 当前使用 invokedynamic的 lambda 实现比单独的内部类方法稍微快一点,因为不需要加载这些内部类文件,而是动态地创建内部类 byte [](例如,为了满足函数接口) ,并缓存以供以后使用。
    1. JVM 团队仍然可以选择生成单独的内部类(通过引用封闭类的静态方法)文件: 这是灵活的