Contents

How Java Lambda Expressions Work Internally

Introduction

In our previous article, we discussed what lambda functions are and why Java introduced them. Now, let’s dive into how Java compiles and implements these functional interfaces under the hood.

Disassembling Lambda’s Byte Code

Let’s start with an example. Using Java’s built in Predicate interface, lets define a lambda called isEven which basically tests if a number is odd or even.

1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = num -> num % 2 == 0;
        System.out.println(isEven.test(4));
    }
}

How JVM compiles the lambda num -> num % 2 == 0? JVM does not create any anonymous class and use that instance. Lets disassemble the byte code to see.

In our another previous article, we discussed about byte codes and how we can disassemble them. In same process if we run javac Main.java and then javap -c -p Main.class, we can see the following byte code:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokedynamic #2,  0              // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
       5: astore_1
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: aload_1
      10: iconst_4
      11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      14: invokeinterface #5,  2            // InterfaceMethod java/util/function/Predicate.test:(Ljava/lang/Object;)Z
      19: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V
      22: return

  private static boolean lambda$main$0(java.lang.Integer);
    Code:
       0: aload_0
       1: invokevirtual #7                  // Method java/lang/Integer.intValue:()I
       4: iconst_2
       5: irem
       6: ifne          13
       9: iconst_1
      10: goto          14
      13: iconst_0
      14: ireturn
}

We can see at line 21, there is a private static method generated which is not present in our source code.

1
private static boolean lambda$main$0(java.lang.Integer);

Java compiler copies the lambda function’s body into a private method, these methods are called synthetic method. If we run javap -p -v Main.class we can see some flags associated with every methods. There is a flag ACC_SYNTHETIC, which define in specifications as: The ACC_SYNTHETIC flag indicates that this method was generated by a compiler and does not appear in source code.

1
2
3
  private static boolean lambda$main$0(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)Z
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC

The method name is in format lambda$METHOD_NAME$COUNTER. Here METHOD_NAME is the method in which the lambda was written in the source code, in our case its in main method. The COUNTER simply a sequence to differentiate multiple lambda implementations in the same method starts from 0. The method is static because in our example its not uses any instance member. If a lambda expression use any instance member, the method will be generated as non-static.

Invocation of Lambda Function

Now lets talk about how this method is invoked. As we can see its in the bytecode, JVM can executes it like other methods, right? Not really. There are five instructions JVM uses to invoke methods:

invokevirtual invokes an instance method of an object
invokeinterface invokes an interface method, searching the methods implemented by the particular run-time object to find the appropriate method
invokespecial invokes an instance method requiring special handling
invokestatic invokes a static method in a named class
invokedynamic invokes the method which is the target of the call site object bound to the invokedynamic instruction

First four instruction’s invocation are statically typed, means that when they invoke a method, they must know the arguments types of the method, as well as the return type must me defined in compile time (Java is statically typed language!).

In our bytecode above, in code index 19 we can see invokevirtual call to java/io/PrintStream.println, its argument is Z which mean boolean, and return type is V means void. So invokevirtual is exactly knows the argument and return type.

1
19: invokevirtual #6        // Method java/io/PrintStream.println:(Z)V

invokedynamic is special instruction proposed for Supporting Dynamically Typed languages in JVM. In dynamically typed languages, the type of a variable is not known until the program is actually executed, so method invocations and type-related operations are resolved at runtime.

In byte code index 0, if we break down the invokedynamic instruction:

1
0: invokedynamic #2,  0     // InvokeDynamic #0:test:()Ljava/util/function/Predicate;

#2 is a reference to a constant pool entry. We discussed about constant pool in previous post. We can see constant pool entries by running javap -p -v Main.class.

1
2
3
    Constant pool:
    #1 = Methodref          #9.#21         // java/lang/Object."<init>":()V
    #2 = InvokeDynamic      #0:#27         // #0:test:()Ljava/util/function/Predicate;

Here #0 in #0:#27 is a reference to a bootstrap method. A Bootstrap method is a piece of code that the JVM can use to perform dynamic invocation. We can also get the bootstrap information from the byte code as follows:

1
2
3
4
5
6
BootstrapMethods:
    0: #23 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:
    #24 (Ljava/lang/Object;)Z
    #25 invokestatic Main.lambda$main$0:(Ljava/lang/Integer;)Z
    #26 (Ljava/lang/Integer;)Z

It refers to LambdaMetafactory.metafactory in java.lang.invoke package. So JVM will invoke this method to perform the dynamic invocation. Byte code also provide the method arguments. If we look into the metafactory source code, the arguemtns matched with the BootstrapMethods in byte code above.

When JVM encounter a invokedynamic instruction, it will invoke the bootstrap method metafactory with arguments also provided by the bytecode. This metafactory then creates the class implementing the lambda body which was generated as private synthetic method discussed above. If we debug our program using IntelliJ IDEA , we can see the arguments passed by the JVM to metafactory in runtime as below:

/images/metafactory.png

We can see the caller is Main our main class, the invokedName is test which is the Predicates method, invokedType is Predicate etc. The return type is CallSite , which act as a container for MethodHandle. MethodHandle is a core part of dynamic invoking. It is a lightweight, high-performance, and flexible reference. It can be used as a direct method reference, allowing fast invocation, argument manipulation, and functional composability.

Through its linkage mechanism, a CallSite resolves and identifies a MethodHandle for the target method to be invoked. Once the target is determined, subsequent calls to the CallSite directly invoke the target method or function without the need for a repeated lookup.

In summary the three major components involve in dynamic invocation:

  1. The LambdaMetafactory creates the implementation of the functional interface and return an instance of the interface as a class instance. Its the a connection between the functional interface and the implementation, i.e. produce Callsite.
  2. Callsite with its linkage mechanism, links the lambda expression to the appropriate implementation method which is MethodHandle.
  3. MethodHandle is a reflective mechanism in Java for invoking methods. It is used to link the lambda expression to the target method. The MethodHandle represents the method that the lambda expression should call when invoked and can be cached & reused.

Creating Demo Lambda Instance Class Using Metafactory

Here is an example of how Metafactory and Callsite implement and link to the target method for a functional interface:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

public class Main {

    private static int lambda$Add(int a, int b) {
        return a + b;
    }
    
    public static void main(String[] args) throws Throwable {
        // lookup helps to find a method from a instance class
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        
        // target method 'lambda$Add' in Main and its imeplementation will be linked to lambda interface.
        MethodHandle addExactMethod = lookup.findStatic(Main.class, "lambda$Add", MethodType.methodType(int.class, int.class, int.class));

        // Create a CallSite using LambdaMetafactory for the Calculator interface above
        CallSite site = LambdaMetafactory.metafactory(
                lookup,
                "calculate", // the method in FunctionalInterface
                MethodType.methodType(Calculator.class),
                addExactMethod.type().changeReturnType(int.class),
                addExactMethod,
                addExactMethod.type()
        );

        // Get the functional interface instance from the CallSite
        Calculator calculator = (Calculator) site.getTarget().invokeExact();

        // Use the lambda expression
        int result = calculator.calculate(5, 7);
        System.out.println("Result: " + result);
    }
}

Advantages of Runtime InvokeDynamic and MethodHandle

There are some Advantages of run time MethodHandle over generating a new class for each lambda. If Java generates new class for each lambda, the classloading load will be increased, which will caused slow start up time. Creating and instantiating classes involves class loading, bytecode generation, and class instantiation, MethodHandle can reduce the extra overhead. Besides, more memory will be needed for extra classes.

MethodHandles are strongly typed, errors related to type mismatches are caught at compile time. InvokeDynamic creates and runs the lambda classes in runtime. It will not be implemented until the first call performed to it. It helps with lazy loading and optimize dynamic method calling.

MethodHandles provide flexibility for composing and function references. We can easily create chains of method handles to perform various operations, filtering, mapping, and combining functions.


Conclusion

The mechanism of Java’s lambda expressions is relatively complex when resolving dynamic implementations, but it offers performance and flexibility for adaptation. To read more about internal ideas, here.