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

Ksh*_*rma 64 java lambda jvm bytecode java-8

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

这对于动态语言很有用,其中确切的方法和参数类型在运行时才知道.但Java lambda不是这种情况.它们被转换为具有明确定义的参数的静态方法.并且可以使用此方法调用此方法invokestatic.

那么invokedynamiclambda 的需求是什么,尤其是在性能受到影响的情况下?

Dan*_*rry 69

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"); 
}
Run Code Online (Sandbox Code Playgroud)

任何lambda都必须成为某个基类或接口的实例.该实例有时包含从原始方法捕获的变量的副本,有时包含指向父对象的指针.这可以作为匿名类实现.

为什么invokedynamic

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

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);
    }
);   
Run Code Online (Sandbox Code Playgroud)

这需要在编译时创建类,然后在运行时加载.jvm运行这些类的方式将与原始类位于同一目录中.并且第一次执行使用该lambda的语句时,必须加载并初始化该匿名类.

关于表现

第一次调用invokedynamic将触发匿名类生成.然后将操作码invokedynamic替换为与手动写入匿名实例化的性能等效的代码.

  • 请参阅[`java.lang.invoke`的包文档](http://docs.oracle.com/javase/8/docs/api/java/lang/invoke/package-summary.html#indyinsn):*Every动态调用站点在第一次调用之前最多从未链接到链接转换一次。无法撤消已完成的引导方法调用的影响。*并尝试了解[签名多态性](http://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandle .html#sigpoly)在进一步讨论之前...... (3认同)
  • 请不要混淆框架的第一次开销和单个`invokedynamic`指令的开销.如果你在循环之前放置一个lambda表达式*然后检查,你会看到循环中的`invokedynamic`指令在第一次调用的链接期间恰好一次调用`findStatic`.这适用于所有其他随后执行的`invokedynamic`指令,并且**指定*在动态调用点的一个完整链接之后,它将永远保持链接.并且方法句柄做*不*把`Object []`作为参数. (2认同)

Edw*_*rzo 44

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

翻译策略

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

使用invokedynamic可以让我们将转换策略的选择推迟到运行时.运行时实现可以自由选择策略来评估lambda表达式.运行时实现选项隐藏在用于lambda构造的标准化(即平台规范的一部分)API之后,因此静态编译器可以发出对此API的调用,并且JRE实现可以选择其首选实现策略.invokedynamic机制允许这样做,而没有这种后期绑定方法可能带来的性能成本.

当编译器遇到lambda表达式时,它首先将lambda体降低(desugars)为一个方法,其参数列表和返回类型与lambda表达式匹配,可能还有一些额外的参数(对于从词法范围捕获的值,如果有的话). )在捕获lambda表达式的点上,它生成一个invokedynamic调用站点,当调用该站点时,返回lambda正在转换的函数接口的实例.该调用站点称为给定lambda的lambda工厂.lambda工厂的动态参数是从词法范围捕获的值.lambda工厂的bootstrap方法是Java语言运行时库中的标准化方法,称为lambda metafactory.静态引导参数在编译时捕获有关lambda的信息(它将被转换的功能接口,desugared lambda主体的方法句柄,有关SAM类型是否可序列化的信息等)

方法引用的处理方式与lambda表达式相同,只是大多数方法引用不需要被置于新方法中; 我们可以简单地为引用的方法加载一个常量方法句柄并将其传递给metafactory.

所以,这里的想法似乎是封装翻译策略,而不是通过隐藏这些细节来提交一种特定的做事方式.在未来,当类型擦除和缺少值类型已经解决并且Java可能支持实际的函数类型时,它们可能也会去那里并将该策略更改为另一个策略而不会在用户代码中引起任何问题.

  • 这是链接:http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html (2认同)

小智 5

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

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

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

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