JVM什么时候决定重用旧的lambda?

请考虑以下代码片段:

public static Object o = new Object(); public static Callable x1() { Object x = o; return () -> x; } public static Callable x2() { return () -> o; } 

方法x2()将始终返回相同的lamba对象,而x1()将始终创建新的lamba对象:

  System.out.println(x1()); System.out.println(x1()); System.out.println(x2()); System.out.println(x2()); 

会打印出这样的东西:

 TestLambda$$Lambda$1/821270929@4a574795 TestLambda$$Lambda$1/821270929@f6f4d33 TestLambda$$Lambda$2/603742814@7adf9f5f TestLambda$$Lambda$2/603742814@7adf9f5f 

哪个(在JVM规范中我猜?)是描述了这个lambda重用规则? JVM如何决定重用与否?

您无法确定为lambda表达式返回的对象的标识。 它可以是新实例,也可以是预先存在的实例。

这在JLS§15.27.4中规定:

在运行时,lambda表达式的计算类似于类实例创建表达式的计算,只要正常完成产生对对象的引用即可。 lambda表达式的评估不同于lambda体的执行。

将分配并初始化具有以下属性的类的新实例,或者引用具有以下属性的类的现有实例。 如果要创建新实例,但没有足够的空间来分配对象,则抛出一个OutOfMemoryError会突然评估lambda表达式。

经过一些调查后,看起来它取决于lambda表达式的创建是通过invokedynamic执行的,而你看到的是invokedynamic如何在Oracle的JVM上运行的副作用。

反编译你的x1()x2()方法:

 public static java.util.concurrent.Callable x1(); Code: stack=1, locals=1, args_size=0 0: getstatic #2 // Field o:Ljava/lang/Object; 3: astore_0 4: aload_0 5: invokedynamic #3, 0 // InvokeDynamic #0:call:(Ljava/lang/Object;)Ljava/util/concurrent/Callable; 10: areturn public static java.util.concurrent.Callable x2(); Code: stack=1, locals=0, args_size=0 0: invokedynamic #4, 0 // InvokeDynamic #1:call:()Ljava/util/concurrent/Callable; 5: areturn 

常量池的相关部分:

  #3 = InvokeDynamic #0:#37 // #0:call:(Ljava/lang/Object;)Ljava/util/concurrent/Callable; #4 = InvokeDynamic #1:#39 // #1:call:()Ljava/util/concurrent/Callable; 

BootstrapMethods:

 0: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #35 ()Ljava/lang/Object; #36 invokestatic Test.lambda$x1$0:(Ljava/lang/Object;)Ljava/lang/Object; #35 ()Ljava/lang/Object; 1: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #35 ()Ljava/lang/Object; #38 invokestatic Test.lambda$x2$1:()Ljava/lang/Object; #35 ()Ljava/lang/Object; 

如下所述:

因为每个invokedynamic指令(通常)链接到不同的调用站点(我们有两个调用站点,每个xN函数一个) ,所以常量池缓存必须包含每个invokedynamic指令的单独条目。 (其他调用指令可以共享CP缓存条目,如果它们在常量池中使用相同的符号引用。)

常量池高速缓存条目(“CPCE”)在被解析时具有一个或两个单词的元数据和/或偏移信息。

对于invokedynamic,已解析的CPCE包含指向具体适配器方法的Method *指针,该方法提供调用的确切行为。 还有一个与呼叫站点相关联的参考参数称为附录,它存储在CPCE的resolved_references数组中。

该方法称为适配器,因为(一般来说)它会对参数进行混洗,从调用站点中提取目标方法句柄,并调用方法句柄。

额外的引用参数称为附录,因为它在执行invokedynamic指令时附加到参数列表中。

通常,附录是bootstrap方法生成的CallSite引用,但JVM并不关心这一点。 只要CPCE中的适配器方法知道如何处理与CPCE一起存储的附录,一切都很好。

作为一个极端情况,如果appendix值为null,则根本不推送它,并且适配器方法不能指望额外的参数。 在这种情况下,适配器方法可以是对静态方法的永久链接引用,其签名与invokedynamic指令一致。 这实际上将invokedynamic转换为一个简单的invokestatic。 许多其他这样的强度降低优化是可能的。

我正在解释“这实际上会转向”意味着在这种情况下(没有参数的适配器),invokedynamic将有效地表现为和invokestatic调用,并且适配器将被缓存并重用。

所有这些都是特定于Oracle的JVM,但我怀疑在这方面,这个是最明显的选择,我希望即使在其他jvm实现中也能看到类似的东西。

另外,检查这个好的答案,以便更清楚地重新引用该引用,比我能够解释它更好。

正如已经指出的那样 ,JLS没有指定实际行为,只要JLS保持满载,就允许从当前实现派生未来版本。

以下是当前版本的HotSpot中发生的情况:

任何lambda表达式都是通过invokedynamic调用站点绑定的。 此调用站点请求引导方法绑定工厂,以实现实现lambda表达式的function接口的实例。 作为参数,执行lambda表达式所需的任何变量都将传递给工厂。 而是将lambda表达式的主体复制到类内部的方法中。

对于您的示例,desuggared版本看起来像下面的代码使用尖括号中的invokedynamic指令剪切:

 class Foo { public static Object o = new Object(); public static Callable x1() { Object x = o; return Bootstrap.(x); } private static Object lambda$x1(Object x) { return x; } public static Callable x2() { return Bootstrap.(); } private static void lambda$x2() { return Foo.o; } } 

然后要求boostrap方法(实际上位于java.lang.invoke.LambdaMetafactory )在第一次调用时绑定调用站点。 对于lambda表达式,此绑定永远不会更改,因此引导方法仅调用一次。 为了能够绑定实现function接口的类,bootstrap方法必须首先在运行时创建一个类,如下所示:

 class Lambda$x1 implements Callable { private static Callable make(Object x) { return new Lambda$x1(x); } private final Object x; // constructor omitted @Override public Object call() { return x; } } class Lambda$x2 implements Callable { @Override public Object call() { return Foo.o; } } 

创建这些类之后, invokedynamic指令必须调用由第一个类定义到调用站点的工厂方法。 对于第二个类,没有创建工厂,因为该类是完全无状态的。 因此,bootstrap方法创建类的单例实例,并将实例直接绑定到调用站点(使用常量MethodHandle )。

为了从另一个类调用静态方法,使用匿名类加载器来加载lambda类。 如果你想了解更多,我最近总结了我对lambda表达式的研究结果 。

但同样, 总是针对规范进行编码,而不是实现 。 这可以改变!

(编辑此为我之前的回答是垃圾!)

本文档http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html提供了解释。

该文件中的这些部分应该有助于回答您的问题……

Desugaring例子 – “无国籍”lambdas

要转换的最简单的lambda表达式是从其封闭范围(无状态lambda)中捕获没有状态的表达式:

…和…

Desugaring示例 – lambdas捕获不可变值

另一种forms的lambda表达式涉及捕获封闭的最终(或有效最终)局部变量,和/或来自封闭实例的字段(我们可以将其视为最终包含此引用的捕获)。

你的第二个方法(x2)是第一种lamba(一种无状态的lamba,它从它的封闭范围中没有捕获任何状态)的一个例子,这可能就是为什么在每种情况下返回相同的lamba。

如果使用javap打印出生成的字节码,您还可以看到生成的两个块之间存在差异…

 >javap -p -c L2.class public class L2 { public static java.lang.Object o; public L2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static java.util.concurrent.Callable x1(); Code: 0: getstatic #2 // Field o:Ljava/lang/Object; 3: astore_0 4: aload_0 5: invokedynamic #3, 0 // InvokeDynamic #0:call:(Ljava/lang/Object;)Ljava/util/concurrent/Callable; 10: areturn public static java.util.concurrent.Callable x2(); Code: 0: invokedynamic #4, 0 // InvokeDynamic #1:call:()Ljava/util/concurrent/Callable; 5: areturn private static java.lang.Object lambda$x2$1() throws java.lang.Exception; Code: 0: getstatic #2 // Field o:Ljava/lang/Object; 3: areturn private static java.lang.Object lambda$x1$0(java.lang.Object) throws java.lang.Exception; Code: 0: aload_0 1: areturn static {}; Code: 0: new #5 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."":()V 7: putstatic #2 // Field o:Ljava/lang/Object; 10: return } 

编译器无法优化x1()以返回相同的lambda – 然后行为会有所不同。 由于o不是final,因此返回的lambda需要捕获该字段的状态(使用x变量),因为它的值可能会在调用x1()和调用返回的lambda之间发生变化。

这并不是说在没有情况下编译器理论上可以重用实例而不是(其他答案对此有所了解) – 只是这不是其中一种情况。