JVM 何时决定重用旧的 lambda?
When does JVM decide to reuse old lambda?
考虑以下代码片段:
public static Object o = new Object();
public static Callable x1() {
Object x = o;
return () -> x;
}
public static Callable x2() {
return () -> o;
}
方法 x2()
将始终 return 相同的兰巴对象,而 x1()
将始终创建新对象:
System.out.println(x1());
System.out.println(x1());
System.out.println(x2());
System.out.println(x2());
将打印出如下内容:
TestLambda$$Lambda/821270929@4a574795
TestLambda$$Lambda/821270929@f6f4d33
TestLambda$$Lambda/603742814@7adf9f5f
TestLambda$$Lambda/603742814@7adf9f5f
在哪里(我猜是在 JVM 规范中?)描述了 lambda 重用规则? JVM 是如何决定重用或不重用的?
(因为我之前的回答是垃圾所以编辑了这个!)
本文档http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html提供了解释。
该文档中的这些部分应该有助于回答您的问题...
Desugaring example -- "stateless" lambdas
The simplest form of lambda expression to translate is one that captures no state from its enclosing scope (a stateless lambda):
...和...
Desugaring example -- lambdas capturing immutable values
The other form of lambda expression involves capture of enclosing final (or effectively final) local variables, and/or fields from enclosing instances (which we can treat as capture of the final enclosing this reference).
您的第二种方法 (x2) 是第一种 lamba 的示例(一种无状态的 lamba,不从其封闭范围捕获任何状态),这可能就是为什么在每种情况下都返回相同的 lamba 的原因。
如果用javap打印出生成的字节码,也可以看到生成的两个block是有区别的...
>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() throws java.lang.Exception;
Code:
0: getstatic #2 // Field o:Ljava/lang/Object;
3: areturn
private static java.lang.Object lambda$x1[=10=](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
}
您无法确定为 lambda 表达式返回的对象的身份。它可以是一个新实例,也可以是一个预先存在的实例。
这在JLS §15.27.4中指定:
At run time, evaluation of a lambda expression is similar to evaluation of a class instance creation expression, insofar as normal completion produces a reference to an object. Evaluation of a lambda expression is distinct from execution of the lambda body.
Either a new instance of a class with the properties below is allocated and initialized, or an existing instance of a class with the properties below is referenced. If a new instance is to be created, but there is insufficient space to allocate the object, evaluation of the lambda expression completes abruptly by throwing an OutOfMemoryError.
经过一些调查,这似乎取决于以下事实: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;
引导方法:
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[=12=]:(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:()Ljava/lang/Object;
#35 ()Ljava/lang/Object;
如解释的那样here:
Because each invokedynamic instruction links (in general) to a
different call site (we have two call sites, one for each xN function), the constant pool cache must contain a separate
entry for each invokedynamic instruction. (Other invoke instructions
can share CP cache entries, if they use the same symbolic reference in
the constant pool.)
A Constant Pool cache entry ("CPCE"), when resolved, has one or two words of
metadata and/or offset information.
For invokedynamic, a resolved CPCE contains a Method* pointer to a
concrete adapter method providing the exact behavior of the call.
There is also a reference parameter associated with the call site
called the appendix, which is stored in the resolved_references array
for the CPCE.
The method is called an adapter because (generally speaking) it
shuffles arguments, extracts a target method handle from the call
site, and invokes the method handle.
The extra reference parameter is called the appendix because it is
appended to the argument list when the invokedynamic instruction is
executed.
Typically the appendix is the CallSite reference produced by the
bootstrap method, but the JVM does not care about this. As long as the
adapter method in the CPCE knows what to do with the appendix stored
with the CPCE, all is well.
As a corner case, if the appendix value is null, it is not pushed at
all, and the adapter method must not expect the extra argument. The
adapter method in this case could be a permanently linked reference to
a static method with a signature consistent with the invokedynamic
instruction. This would in effect turn the invokedynamic into a simple
invokestatic. Many other such strength reduction optimizations are
possible.
我将 "This would in effect turn" 解释为在这种情况下(没有参数的适配器) invokedynamic 将有效地表现得像 invokestatic 调用并且适配器将被缓存和重用。
所有这些都是特定于 Oracle 的 JVM,但我怀疑关于这方面,这是最明显的选择,我希望即使在其他 jvm 实现中也能看到类似的东西。
此外,请检查这个很好的 answer 以更清晰地改写该引用,比我能够解释它的方式更好。
与 already pointed out 一样,JLS 未指定实际行为,只要 JLS 保持完整,就允许从当前实现派生未来版本。
这是当前版本的 HotSpot 中发生的事情:
任何 lambda 表达式都通过 invokedynamic 调用站点绑定。此调用站点请求一个 bootstrap 方法来为实现 lambda 表达式的功能接口的实例绑定工厂。作为参数,执行 lambda 表达式所需的任何变量都被传递给工厂。 lambda 表达式的主体被复制到 class 内部的方法中。
对于您的示例,脱糖版本看起来像以下代码,在尖括号中用 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,bootstrap 方法必须首先在运行时创建一个 class,如下所示:
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; }
}
创建这些classes后,invokedynamic指令绑定调用第一个class定义的工厂方法到调用点.对于第二个 class,没有创建工厂,因为 class 是完全无状态的。因此,bootstrap 方法创建了 class 的单例实例,并将该实例直接绑定到调用站点(使用常量 MethodHandle
)。
为了从另一个 class 调用静态方法,匿名 class 加载程序用于加载 lambda classes。如果你想了解更多,我最近summarized my findings on lambda expressions。
但是,总是根据规范编写代码,而不是实现。这可以改变!
编译器无法将相同的 lambda x1()
优化为 return —— 行为会有所不同。由于 o
不是最终的,lambda returned 需要捕获该字段的状态(使用 x
变量),因为它的值可能会在调用 x1()
和调用returned lambda.
这并不是说编译器在理论上 可以 重用实例但没有重用实例的情况并不存在(其他答案对此提供了一些见解) - 只是这不是其中一种情况。
考虑以下代码片段:
public static Object o = new Object();
public static Callable x1() {
Object x = o;
return () -> x;
}
public static Callable x2() {
return () -> o;
}
方法 x2()
将始终 return 相同的兰巴对象,而 x1()
将始终创建新对象:
System.out.println(x1());
System.out.println(x1());
System.out.println(x2());
System.out.println(x2());
将打印出如下内容:
TestLambda$$Lambda/821270929@4a574795
TestLambda$$Lambda/821270929@f6f4d33
TestLambda$$Lambda/603742814@7adf9f5f
TestLambda$$Lambda/603742814@7adf9f5f
在哪里(我猜是在 JVM 规范中?)描述了 lambda 重用规则? JVM 是如何决定重用或不重用的?
(因为我之前的回答是垃圾所以编辑了这个!)
本文档http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html提供了解释。
该文档中的这些部分应该有助于回答您的问题...
Desugaring example -- "stateless" lambdas
The simplest form of lambda expression to translate is one that captures no state from its enclosing scope (a stateless lambda):
...和...
Desugaring example -- lambdas capturing immutable values
The other form of lambda expression involves capture of enclosing final (or effectively final) local variables, and/or fields from enclosing instances (which we can treat as capture of the final enclosing this reference).
您的第二种方法 (x2) 是第一种 lamba 的示例(一种无状态的 lamba,不从其封闭范围捕获任何状态),这可能就是为什么在每种情况下都返回相同的 lamba 的原因。
如果用javap打印出生成的字节码,也可以看到生成的两个block是有区别的...
>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() throws java.lang.Exception;
Code:
0: getstatic #2 // Field o:Ljava/lang/Object;
3: areturn
private static java.lang.Object lambda$x1[=10=](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
}
您无法确定为 lambda 表达式返回的对象的身份。它可以是一个新实例,也可以是一个预先存在的实例。
这在JLS §15.27.4中指定:
At run time, evaluation of a lambda expression is similar to evaluation of a class instance creation expression, insofar as normal completion produces a reference to an object. Evaluation of a lambda expression is distinct from execution of the lambda body.
Either a new instance of a class with the properties below is allocated and initialized, or an existing instance of a class with the properties below is referenced. If a new instance is to be created, but there is insufficient space to allocate the object, evaluation of the lambda expression completes abruptly by throwing an OutOfMemoryError.
经过一些调查,这似乎取决于以下事实: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;
引导方法:
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[=12=]:(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:()Ljava/lang/Object;
#35 ()Ljava/lang/Object;
如解释的那样here:
Because each invokedynamic instruction links (in general) to a different call site (we have two call sites, one for each xN function), the constant pool cache must contain a separate entry for each invokedynamic instruction. (Other invoke instructions can share CP cache entries, if they use the same symbolic reference in the constant pool.)
A Constant Pool cache entry ("CPCE"), when resolved, has one or two words of metadata and/or offset information.
For invokedynamic, a resolved CPCE contains a Method* pointer to a concrete adapter method providing the exact behavior of the call. There is also a reference parameter associated with the call site called the appendix, which is stored in the resolved_references array for the CPCE.
The method is called an adapter because (generally speaking) it shuffles arguments, extracts a target method handle from the call site, and invokes the method handle.
The extra reference parameter is called the appendix because it is appended to the argument list when the invokedynamic instruction is executed.
Typically the appendix is the CallSite reference produced by the bootstrap method, but the JVM does not care about this. As long as the adapter method in the CPCE knows what to do with the appendix stored with the CPCE, all is well.
As a corner case, if the appendix value is null, it is not pushed at all, and the adapter method must not expect the extra argument. The adapter method in this case could be a permanently linked reference to a static method with a signature consistent with the invokedynamic instruction. This would in effect turn the invokedynamic into a simple invokestatic. Many other such strength reduction optimizations are possible.
我将 "This would in effect turn" 解释为在这种情况下(没有参数的适配器) invokedynamic 将有效地表现得像 invokestatic 调用并且适配器将被缓存和重用。
所有这些都是特定于 Oracle 的 JVM,但我怀疑关于这方面,这是最明显的选择,我希望即使在其他 jvm 实现中也能看到类似的东西。
此外,请检查这个很好的 answer 以更清晰地改写该引用,比我能够解释它的方式更好。
与 already pointed out 一样,JLS 未指定实际行为,只要 JLS 保持完整,就允许从当前实现派生未来版本。
这是当前版本的 HotSpot 中发生的事情:
任何 lambda 表达式都通过 invokedynamic 调用站点绑定。此调用站点请求一个 bootstrap 方法来为实现 lambda 表达式的功能接口的实例绑定工厂。作为参数,执行 lambda 表达式所需的任何变量都被传递给工厂。 lambda 表达式的主体被复制到 class 内部的方法中。
对于您的示例,脱糖版本看起来像以下代码,在尖括号中用 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,bootstrap 方法必须首先在运行时创建一个 class,如下所示:
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; }
}
创建这些classes后,invokedynamic指令绑定调用第一个class定义的工厂方法到调用点.对于第二个 class,没有创建工厂,因为 class 是完全无状态的。因此,bootstrap 方法创建了 class 的单例实例,并将该实例直接绑定到调用站点(使用常量 MethodHandle
)。
为了从另一个 class 调用静态方法,匿名 class 加载程序用于加载 lambda classes。如果你想了解更多,我最近summarized my findings on lambda expressions。
但是,总是根据规范编写代码,而不是实现。这可以改变!
编译器无法将相同的 lambda x1()
优化为 return —— 行为会有所不同。由于 o
不是最终的,lambda returned 需要捕获该字段的状态(使用 x
变量),因为它的值可能会在调用 x1()
和调用returned lambda.
这并不是说编译器在理论上 可以 重用实例但没有重用实例的情况并不存在(其他答案对此提供了一些见解) - 只是这不是其中一种情况。