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.
|
|
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:
|
|
We can see at line 21, there is a private static method generated which is not present in our source code.
|
|
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.
|
|
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.
|
|
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:
|
|
#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
.
|
|
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:
|
|
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:
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:
- 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. Callsite
with its linkage mechanism, links the lambda expression to the appropriate implementation method which is MethodHandle.MethodHandle
is a reflective mechanism in Java for invoking methods. It is used to link the lambda expression to the target method. TheMethodHandle
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:
|
|
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.