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

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

这在dynamic语言中非常有用,在运行时,确切的方法和参数types是不知道的。 但是Java lambda不是这种情况。 它们被翻译成具有明确定义的参数的静态方法。 而且这个方法可以使用invokestatic来调用。

那么对lambdaexpression式invokedynamic的需求是什么,特别是当性能受到影响的时候呢?

不使用invokedynamic调用invokedynamic ,使用invokedynamic创build对象表示,实际的调用是常规的invokeinterfaceinvokeinterface

例如:

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

为什么invokedynamic

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

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

使用invokedynamic在运行时构造匿名类允许JVM在运行时生成该类字节码。 随后对同一语句的调用使用caching版本。 使用invokedynamic的另一个原因是能够在将来改变实现策略而不必改变已编译的代码。

没有采取的道路

另一个选项是编译器为每个lambda实例创build一个内部类,相当于将上面的代码翻译成:

 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); } ); 

这要求在编译时创build类,并在运行时加载。 jvm工作的方式将与原始类位于同一个目录中。 而且第一次执行使用该lambdaexpression式的语句时,该匿名类将不得不被加载和初始化。

关于表演

第一次调用invokedynamic将触发匿名类生成。 然后,将操作码invokedynamicreplace为与手动写入匿名实例相当的代码 。

Brain Goetz在他的一篇论文中解释了lambda翻译策略的原因。 幸运的是我保留了一个副本:

翻译策略

有很多方法可以表示字节码中的lambdaexpression式,比如内部类,方法句柄,dynamic代理等等。 每种方法都有优点和缺点。 在select策略时,有两个相互竞争的目标:通过不采取特定的策略来提高未来优化的灵活性,同时提供类文件表示的稳定性。 通过使用JSR 292中的invokedynamic特性,我们可以实现这两个目标,将字节码中的lambda创build的二进制表示与运行时评估lambdaexpression式的机制分开。 我们不用生成字节码来创build实现lambdaexpression式的对象(例如调用内部类的构造函数),而是描述构造lambda的配方,并将实际构造委托给语言运行库。 该配方被编码在invokedynamic指令的静态和dynamic参数列表中。

invokedynamic的使用让我们推迟翻译策略的select,直到运行时间。 运行时实现可以dynamic地select一个策略来评估lambdaexpression式。 运行时实现选项隐藏在用于lambda构造的标准化(即平台规范的一部分)API之后,以便静态编译器可以发出对此API的调用,并且JRE实现可以select他们的首选实现策略。 invokedynamic机制允许这样做,而没有性能成本,这个后期绑定方法可能会强加。

当编译器遇到一个lambdaexpression式时,它首先将lambda体降低(去除)到一个方法中,该方法的参数列表和返回types与lambdaexpression式匹配,可能带有一些附加参数(对于从词法作用域捕获的值,如果有的话)。 )在捕获lambdaexpression式的时候,它会生成一个invokedynamic调用位置,当调用它时,将返回lambda转换到的函数接口的一个实例。 这个调用站点被称为给定lambda的lambda工厂。 lambda工厂的dynamic参数是从词法作用域捕获的值。 lambda工厂的引导方法是Java语言运行库中的标准化方法,称为lambda元数据集。 静态引导参数在编译时捕获有关lambda的已知信息(它将被转换到的函数接口,desugared lambda体的方法句柄,关于SAMtypes是否可序列化的信息等)

方法引用与lambdaexpression式的处理方式相同,除了大多数方法引用不需要被parsing成新的方法; 我们可以简单地为引用的方法加载一个常量方法句柄,并将其传递给该元数据。

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

目前的Java 8的lambda实现是一个复合决定:

    1. 在封闭类中将lambdaexpression式编译为静态方法; 而不是编译lambda来分离内部类文件(Scala编译这种方式,所以很多$$$类文件)
    1. 引入一个常量池:BootstrapMethods,将静态方法调用包装为callsite对象(可以被caching供以后使用)

所以要回答你的问题,

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