Lambdas are closures, which means they capture their surrounding state, but they don’t actually capture anything in the traditional sense.

Let’s watch a simple lambda in action.

import java.util.function.Function;

public class LambdaExample {
    public static void main(String[] args) {
        int offset = 10;
        Function<Integer, Integer> addOffset = x -> x + offset;
        System.out.println(addOffset.apply(5)); // Output: 15
    }
}

When you compile this, the offset variable isn’t copied into the lambda. Instead, the offset variable is accessed directly. This is where invokeDynamic comes in.

Before invokeDynamic, Java had a few ways to handle dynamic method invocation, but they were often clunky or inefficient for the kind of dynamic behavior lambdas require. invokeDynamic was introduced in Java 7 specifically to make these kinds of dynamic operations, like method calls on dynamically created objects or method references, more efficient and flexible. It allows the JVM to defer the binding of a method call until runtime, delegating the actual lookup and invocation to a "bootstrap method."

Here’s how it works for our lambda:

  1. Compilation: When the Java compiler sees x -> x + offset, it doesn’t generate code to copy offset into a new object. Instead, it generates an invokedynamic bytecode instruction. This instruction points to a specific "bootstrap method" and a "name and descriptor" that describe the method to be invoked.

  2. Bootstrap Method: The bootstrap method is the key. It’s a method that runs once when the invokedynamic instruction is first encountered. Its job is to:

    • Determine the actual method to be called.
    • Generate a specialized, efficient method handle for that call.
    • Link this method handle to the invokedynamic instruction.

    For lambdas, the JVM’s built-in bootstrap method (usually java.lang.invoke.LambdaMetafactory.metaFactory) analyzes the lambda expression. It figures out:

    • The type of the functional interface (Function<Integer, Integer>).
    • The target method of the lambda (the apply method of Function).
    • The captured variables (offset).
  3. Method Handle Generation: The LambdaMetafactory then generates a new class on the fly (or uses an existing one if possible). This class will contain a method that implements the lambda’s logic. Crucially, this generated method will have direct access to the offset variable from the enclosing scope. It doesn’t need to copy it. The metaFactory returns a CallSite object that holds a MethodHandle pointing to this generated implementation.

  4. Invocation: The invokedynamic instruction now has a MethodHandle that directly invokes the lambda’s implementation. The first time addOffset.apply(5) is called, the invokedynamic instruction uses the CallSite to find the MethodHandle. Subsequent calls use the cached MethodHandle directly, making it as fast as a regular method call.

The magic of invokeDynamic for lambdas is that it allows the JVM to create efficient, specialized code at runtime without the overhead of traditional reflection or creating explicit anonymous inner classes for every lambda instance. The LambdaMetafactory is incredibly smart; it can generate code that directly accesses captured variables, making lambdas performant and behave like true closures.

What most people don’t realize is that the LambdaMetafactory generates an entire class dynamically for your lambda. This class is optimized for the specific context, including the captured variables and the target method. It’s not just a simple method pointer; it’s a fully realized implementation that the JVM can execute with high efficiency.

The next step is understanding how method references (::) leverage invokeDynamic in a very similar, yet distinct, way.

Want structured learning?

Take the full Jvm course →