为什么使用invokedynamic调用Java 8 lambda?


69

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

这对于动态语言非常有用,在动态语言中直到运行时才知道确切的方法和参数类型。但是Java lambda并非如此。它们被转换为带有定义明确的参数的静态方法。并且可以使用调用此方法invokestatic

那么,invokedynamic对于lambda的需求是什么,尤其是在性能受到影响时?


8
布莱恩·格茨(Brian Goetz)在这里进行了解释:wiki.jvmlangsummit.com/images/1/1e/2011_Goetz_Lambda.pdf
JB Nizet,

7
invokedynamic不是用来调用拉姆达,但在给定的调用网站创建和绑定一个MethodHandle以拉姆达的身体。
2015年

9
Indy用于捕获lambda,而不是调用它。
布莱恩·格茨

8
我们之所以使用invokedynamic它,是因为它比“显而易见的”翻译方案提供了性能改进,而不是性能降低。(我不知道您会在哪里遇到性能下降的想法,但这是不正确的。)
Brian Goetz

4
stackoverflow.com/questions/19001241/…上进行了性能测试:“最重要的是,在当前Hostpot JVM实现上,JIT编译后,lambda和匿名类的执行情况类似”。
Vadzim 2015年

Answers:


75

Lambda不能使用调用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都必须成为某些基类或接口的实例。该实例有时包含从原始方法捕获的变量的副本,有时还包含指向父对象的指针。可以将其实现为匿名类。

为什么调用动态

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

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替换为性能与手动编写匿名实例化等效的代码


1
是什么支持您的说法,即“重复调用invokedynamic将产生更多垃圾……”或重复执行同一invokedynamic条指令会产生任何垃圾?
Holger 2015年

1
看来您不明白自己所看到的。java.lang.invoke框架的初始化引起了很多调用,这些调用与您的lambda表达式完全无关。步过,直到i==1看到多少次你的断点被触发,然后
Holger 2015年

确实。您知道呼叫站点发生了什么吗?我想确保在恢复我的答案之前,invokedynamic没有被创建新Object []数组的东西所代替。并MethodHandle.invokeExact确实使用Object []作为参数。
Daniel Sperry

2
请不要将框架的第一次开销与一条invokedynamic指令的开销混淆。如果在循环之前放置一个lambda表达式并进行检查,您将看到在第一次调用的链接过程中invokedynamic,循环中的指令将findStatic恰好调用一次。这也适用于所有其他后续执行的invokedynamic指令,并且指定在动态呼叫站点完成一个链接之后,它将永远保持链接状态。和方法处理也没有采取Object[]为参数。
Holger

1
请参阅的软件包文档java.lang.invoke每个动态调用站点在第一次调用之前最多只能从未链接转换为链接。无法撤消已完成的引导方法调用的影响。并在进一步讨论之前尝试理解Signature多态性……
Holger 2015年

47

Brain Goetz在他的一篇论文中解释了lambda翻译策略的原因,不幸的是,现在看来这些论文不可用。幸运的是,我保留了一份副本:

翻译策略

我们可以通过多种方式在字节码中表示lambda表达式,例如内部类,方法句柄,动态代理等。这些方法各有利弊。在选择策略时,有两个相互竞争的目标:通过不承诺特定策略来最大化将来优化的灵活性,以及​​在类文件表示中提供稳定性。通过使用来自JSR 292的invokedynamic功能,将字节码中的lambda创建的二进制表示与在运行时评估lambda表达式的机制分开,我们可以实现这两个目标。我们没有描述生成用于实现lambda表达式的对象的字节码(例如,调用内部类的构造函数),而是描述了构造lambda的方法,并将实际构造委托给语言运行时。该配方被编码在invokedynamic指令的静态和动态参数列表中。

使用invokedynamic可以使我们将翻译策略的选择推迟到运行时为止。运行时实现可以自由选择动态选择策略来评估lambda表达式。运行时实现的选择隐藏在用于lambda构建的标准化(即,平台规范的一部分)API的后面,以便静态编译器可以发出对此API的调用,并且JRE实现可以选择其首选实现策略。调用动力学机制允许完成此操作,而无需使用这种后期绑定方法可能会带来的性能成本。

当编译器遇到lambda表达式时,它首先将lambda主体降低(降低)到其参数列表和返回类型与lambda表达式的参数列表和返回类型匹配的方法中,并可能带有一些其他参数(用于从词法范围捕获的值,如果有的话)。 )在捕获lambda表达式时,它会生成一个invokedynamic调用站点,该站点在被调用时将返回lambda转换成的功能接口的实例。此调用站点称为给定lambda的lambda工厂。lambda工厂的动态参数是从词法范围捕获的值。lambda工厂的bootstrap方法是Java语言运行时库中的一种标准化方法,称为lambda元工厂。

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

因此,这里的想法似乎是封装翻译策略,而不是通过隐藏那些细节来致力于某种特定的处理方式。将来,当解决类型擦除和缺少值类型的问题得到解决,并且Java可能支持实际的函数类型时,他们也可能会去那里并将其更改为另一种策略,而不会对用户代码造成任何问题。



6

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

    1. 将lambda表达式编译为封闭类中的静态方法;而不是编译lambda来分隔内部类文件(Scala以此方式编译,会生成许多$$$类文件)
    1. 引入一个常量池:BootstrapMethods,它将静态方法调用包装到callsite对象中(可以缓存以备后用)

因此,为了回答您的问题,

    1. 当前使用的lambda实现invokedynamic比单独的内部类方法要快一点,因为不需要加载这些内部类文件,而是可以动态创建内部类byte [](例如,满足Function接口),并且缓存以备后用。
    1. JVM团队仍然可以选择生成单独的内部类(通过引用封闭类的静态方法)文件:它很灵活
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.