JVM 何时决定重用旧的 lambda?

2022-09-02 04:02:11

请考虑以下代码片段:

public static Object o = new Object();

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

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

方法将始终返回相同的 lamba 对象,同时始终创建新的对象:x2()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如何决定在哪里重用?


答案 1

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

这在 JLS §15.27.4 中指定:

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

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


答案 2

经过一些调查,它似乎取决于lambda表达式的创建是通过expterdynamic执行的事实,并且您看到的是expturaldynamic在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;

如此处所述:

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

常量池缓存条目 (“CPCE”) 在解析后,具有一个或两个字的元数据和/或偏移信息。

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

该方法称为适配器,因为(一般来说)它会随机排列参数,从调用站点中提取目标方法句柄,然后调用方法句柄。

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

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

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

我将“这将反过来”解释为在这种情况下(没有参数的适配器),invokedynamic将有效地表现得像和调用静态调用,并且适配器将被缓存和重用。

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

另外,检查这个好答案,以便对这句话进行更清晰的改写,这比我能够解释它的方式要好得多。


推荐