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

时间:2015-04-16 12:24:23

标签: java lambda jvm java-8

考虑以下代码片段:

public static Object o = new Object();

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

public static Callable x2() {
    return () -> o;
}

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

    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如何确定重用的位置?

5 个答案:

答案 0 :(得分:10)

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

这在JLS §15.27.4中指定:

  

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

     

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

答案 1 :(得分:5)

经过一些调查,看起来它取决于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;

正如here所说:

  

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

     

常量池缓存条目(" CPCE")解析后,有一个或两个单词   元数据和/或偏移信息。

     

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

     

该方法称为适配器,因为(一般来说)它   shuffles参数,从调用中提取目标方法句柄   site,并调用方法句柄。

     

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

     

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

     

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

我正在解释"这将生效转向"因为在这种情况下(没有参数的适配器),invokedynamic将有效地表现为和invokestatic调用,并且适配器将被缓存并重用。

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

另外,请检查此好answer,以便更清楚地重新引用该引用,这比我能够解释它更好。

答案 2 :(得分:5)

由于它是already pointed out,JLS未指定实际行为,只要JLS保持满填,就允许从当前实现派生未来版本。

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

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

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

class Foo {
  public static Object o = new Object();

  public static Callable x1() {
    Object x = o;
    return Bootstrap.<makeCallable>(x);
  }

  private static Object lambda$x1(Object x) { return x; }

  public static Callable x2() {
    return Bootstrap.<makeCallable>();
  }

  private static void lambda$x2() { return Foo.o; }
}

然后要求boostrap方法(实际位于java.lang.invoke.LambdaMetafactory中)在第一次调用时绑定调用站点。对于lambda表达式,此绑定永远不会更改,因此引导方法仅调用一次。为了能够绑定实现功能接口的类,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类。如果你想了解更多,我最近summarized my findings on lambda expressions

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

答案 3 :(得分:3)

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

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

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

  

Desugaring示例 - “无国籍”lambdas

     

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

...和...

  

Desugaring示例 - lambdas捕获不可变值

     

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

你的第二种方法(x2)是第一种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."<init>":()V
       4: return

  public static java.util.concurrent.Callable<java.lang.Object> 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<java.lang.Object> 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."<init>":()V
       7: putstatic     #2                  // Field o:Ljava/lang/Object;
      10: return
}

答案 4 :(得分:0)

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

这并不是说在没有情况下编译器理论上可以重用该实例但是没有(其他答案给出了一些见解) - 只是这不是其中一个案例。