The idea: a lambda is a call site that links itself once
javac does not turn x -> x * 2 into a class. It moves the body into a private static method, lambda$main$0, and at the place where the lambda is created it writes one instruction: invokedynamic. That instruction does not say which code to run. It names a bootstrap method that will decide, the first time the instruction runs.
On the canvas the class file is on the left, the running main() frame and its two call sites in the middle, and metaspace, heap and stdout on the right. Press Next Call to run to the next call instruction, or Run Iteration for one pass of the loop. The lambda menu switches between a lambda that captures nothing and one that captures k.
What javac writes
- In
main:invokedynamic #0 applyAsInt:()IntUnaryOperator. The name is the interface method; the type says "takes nothing, returns anIntUnaryOperator". (Realjavapprintsinvokedynamic #7, 0 // InvokeDynamic #0:applyAsInt:()Ljava/util/function/IntUnaryOperator;; the page shortens it.) - In the BootstrapMethods attribute, entry
#0: the method handleLambdaMetafactory.metafactoryand three static arguments: the erased interface method type(I)I, a handle tolambda$main$0, and the type the call will have, again(I)I. - The body as
private static synthetic int lambda$main$0(int).
Calling the lambda later (f.applyAsInt(n)) is an ordinary invokeinterface. Only making the lambda object uses invokedynamic.
The first call: bootstrap and link
Every invokedynamic instruction in a class is its own call site, and it starts unlinked. The first time it runs (Demo: first call bootstraps the lambda site):
- The JVM resolves the BootstrapMethods entry: the bootstrap method handle and its static arguments.
- It calls the bootstrap method with a
MethodHandles.LookupforShop(so it may see the privatelambda$main$0), the nameapplyAsInt, the call type, and the static arguments. metafactorywrites the bytes of a small class that implementsIntUnaryOperatorand whoseapplyAsIntcallslambda$main$0, and defines it withLookup.defineHiddenClass. A hidden class has no class file, cannot be found by name and can be unloaded with its owner.- It returns a
ConstantCallSitewhose target is a method handle that produces the lambda object. - The JVM binds that call site to the instruction for good and invokes the target.
Later calls go straight through
On the second and third pass of the loop the same instructions run again, but the call sites are linked: the JVM calls the target directly. There is no bootstrap method, no new class, and for this lambda not even a new object (Demo: later iterations go straight through: 6 invokedynamic runs, 2 bootstrap calls, 4 fast-path runs). Because the target of a ConstantCallSite can never change, the JIT treats it like a constant and inlines through it, so a linked lambda call site costs about the same as a new or a field read.
Capturing and non-capturing lambdas
x -> x * 2 | x -> x * k | |
|---|---|---|
| invokedynamic type | ()IntUnaryOperator | (I)IntUnaryOperator: k is pushed first |
| Body method | lambda$main$0(int x) | lambda$main$0(int k, int x) |
| Hidden class | no fields | final int arg$1 holds k |
| Call site target | a constant: the one instance made at link time | a constructor handle: new Shop$$Lambda(k) |
| Objects after 3 passes | 1 | 3 (Demo: capturing lambda) |
Either way the bootstrap runs once and only one hidden class exists. A captured variable must be effectively final, because the lambda gets a copy of its value, not the variable.
String concatenation with StringConcatFactory
Since Java 9 (JEP 280), javac compiles "f(" + n + ")=" + r to invokedynamic #1 makeConcatWithConstants:(II)String, with entry #1 naming StringConcatFactory.makeConcatWithConstants and one static argument, the recipe "f(\u0001)=\u0001": each \u0001 is a slot for an argument, the rest is constant text. The first run bootstraps a chain of method handles that computes the exact length, fills one byte[] and wraps it in a String, with no StringBuilder and no class file (Demo: string concat via StringConcatFactory). Later runs call that chain directly. The JDK can change the strategy without recompiling your code, because the strategy lives in the bootstrap, not in the class file.
Why invokedynamic and not inner classes
| Anonymous inner class | Lambda with invokedynamic | |
|---|---|---|
| Class files | one per lambda (Shop$1.class), loaded at start | none; a hidden class is spun on the first call |
| Cost if never run | class file in the jar | nothing: the call site is never bootstrapped |
| Instances | new Shop$1() each time | non-capturing: one shared instance |
| Strategy | fixed in the bytecode | chosen by the JDK in LambdaMetafactory |
How the class file and the interpreter work in general is on How a Java Program Is Compiled and Run; ordinary class loading is on How Java Loads a Class, and how the JIT inlines a hot call site is on HotSpot JIT Tiers.
What the page leaves out
Method references (String::length, same mechanism with a handle to an existing method), serializable lambdas (altMetafactory), MutableCallSite and VolatileCallSite used by language runtimes such as JRuby and Nashorn, ConstantDynamic (ldc of a bootstrapped constant), switch on patterns and records' toString (ObjectMethods), CDS archiving of lambda classes, the exact JDK concat strategy (it changed again in JDK 23), and BootstrapMethodError when a bootstrap throws. Timing is not to scale.
See where this page fits on the Java & the JVM mind map.