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

And*_*niy 21 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;
}
Run Code Online (Sandbox Code Playgroud)

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

    System.out.println(x1());
    System.out.println(x1());
    System.out.println(x2());
    System.out.println(x2());
Run Code Online (Sandbox Code Playgroud)

会打印出这样的东西:

TestLambda$$Lambda$1/821270929@4a574795
TestLambda$$Lambda$1/821270929@f6f4d33
TestLambda$$Lambda$2/603742814@7adf9f5f
TestLambda$$Lambda$2/603742814@7adf9f5f
Run Code Online (Sandbox Code Playgroud)

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

Roh*_*ain 11

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

这在JLS§15.27.4中规定:

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

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

  • @Andremoniy嗯,问题可能不重复,但两者都有相同的答案. (3认同)

Umb*_*ndi 6

经过一些调查后,看起来它取决于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
Run Code Online (Sandbox Code Playgroud)

常数池的相关部分:

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

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

正如解释在这里:

因为每个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实现中也会看到类似的东西.

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


Raf*_*ter 5

正如已经指出的那样,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; }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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