Well, to be precise both heap and stack are involved in the lambda/closure construction process. To build the mind model of the closure you can think of it as creating an instance of a class for each lambda occurrence and passing to the constructor of that class all variables from the parent scopes which are accessed by the lambda. However, lets try to go through an example to see what exactly JVM does when building a closure for a lambda :
public void performLambdasDemo() {
// Declare variables which are going to be used in the lambda closure
final Pair methodPairIntegerValue = new Pair(RandomUtils.nextInt(), RandomUtils.nextInt());
final Integer methodIntegerValue = RandomUtils.nextInt();
// Declare lambda
final Supplier methodSupplierLambda = () -> {
return methodPairIntegerValue.fst + 9000 + methodIntegerValue.intValue();
};
// Declare anonymous class
final Supplier methodSupplierInnerClass = new Supplier() {
@Override
public Integer get() {
return methodPairIntegerValue.fst + 9001 + methodIntegerValue.intValue();
}
};
System.out.println(methodSupplierLambda.get());
System.out.println(methodSupplierInnerClass.get());
}
What this useless code does is actually building an instance of a lambda and anonymous inner class doing exactly same. Now lets go through the corresponding byte code for both.
Lambdas
Below is the byte-code generated for the lambda :
L2
LINENUMBER 35 L2
ALOAD 1
ALOAD 2
INVOKEDYNAMIC get(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/util/function/Supplier; [
// handle kind 0x6 : 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;
// arguments:
()Ljava/lang/Object;,
// handle kind 0x6 : INVOKESTATIC
com/sfl/stackoverflow/LambdasExperiment.lambda$performLambdasDemo$1(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/lang/Integer;,
()Ljava/lang/Integer;
]
ASTORE 3
L3
// Omit quite some byte-code and jump to the method declaration
// access flags 0x100A
private static synthetic lambda$performLambdasDemo$1(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/lang/Integer;
L0
LINENUMBER 36 L0
ALOAD 0
GETFIELD com/sun/tools/javac/util/Pair.fst : Ljava/lang/Object;
CHECKCAST java/lang/Integer
INVOKEVIRTUAL java/lang/Integer.intValue ()I
SIPUSH 9000
IADD
ALOAD 1
INVOKEVIRTUAL java/lang/Integer.intValue ()I
IADD
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ARETURN
MAXSTACK = 2
MAXLOCALS = 2
Despite being written in Java byte-code the code above is quite self explaining :
ALOAD 1
ALOAD 2
These two commands push the references methodPairIntegerValue
and methodIntegerValue
to the stack (here is were stack part comes in). This is followed by the INVOKEDYNAMIC
command. This command is the main differentiating factor of the lambdas from the anonymous inner classes. If for the anonymous inner classes an explicit new class in generated in the byte-code, for lambdas the actual implementation is postponed till the runtime of the application. However, most modern JVMs when spotting INVOKEDYNAMIC
generate a new class which has two properties capturing the values pushed to the stack prior the INVOKEDYNAMIC
and create a new instance of it (and here where extra heap usage jumps in). It is worth mentioning that these actions are not directly performed by the INVOKEDYNAMIC
but rather by LambdaMetafactory
to which the call is being delegated to. So the end output is quite similar as it would have been for the anonymous inner class (JVMs are free to change this implementation detail incorporated by LambdaMetafactory
in the future).
private static synthetic lambda$performLambdasDemo$1(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/lang/Integer;
This is a static method containing actual code of the lambda expression. It is going to be invoked by a class LambdaMetafactory
generates during the INVOKEDYNAMIC
call. As you see what it does is pulling 2 values from the stack and performing actual summation.
Anonymous classes
Bellow is the byte-code for the usage of the anonymous class, things are simpler here, hence I have added only the initiation part of the anonymous class and omitted the byte-code for the actual class:
L3
LINENUMBER 39 L3
NEW com/sfl/stackoverflow/LambdasExperiment$2
DUP
ALOAD 0
ALOAD 1
ALOAD 2
INVOKESPECIAL com/sfl/stackoverflow/LambdasExperiment$2. (Lcom/sfl/stackoverflow/LambdasExperiment;Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)V
ASTORE 4
L4
What the code does is pushing the values of this
, methodPairIntegerValue
, methodIntegerValue
to the stack and invoking the constructor of a anonymous class which captures these values in the fields of the anonymous class.
As it is seen from the code snippets above memory footprint wise lambdas and anonymous inner classes are quite similar.
Summary
Coming back to your question :
The references used in the closure are passed around using the stack.
The instance of the generated anonymous class together with its fields holding the references of the variables used in the closure are stored in the heap (as it would if you would explicitly use a class instead of lambda and pass the values via a constructor).
However, there is some difference in the performance of lambdas and anonymous inner classes with regards the bootstrap process and JIT. Following links cover the topic in a great detail:
Hope this helps (despite the answer being a bit lengthy)