After some investigations, it looks like it depends on the fact that the creation of lambda expressions is performed through invokedynamic and what you see is a side-effect of how invokedynamic behaves on the Oracle's JVM.
Decompiling your x1()
and x2()
methods:
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
Relevant section of the Constant pool:
#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;
As explained 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.
I'm interpreting that "This would in effect turn" as meaning that in such circumstances (adapter with no parameters) the invokedynamic will effectively behave like and invokestatic call and that the adapter will be cached and reused.
All this is specific to the Oracle's JVM, but i suspect that regarding to this aspect, this one is the most obvious choice and i'd expect to see something like this even in other jvm implementations.
Also, check this good answer for a cleaner rephrasing of that quote, way better than how i'd be able to explain it.