An Introduction to Invoke Dynamic in the JVM

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

1. Overview

Invoke Dynamic (Also known as Indy) was part of JSR 292 intended to enhance the JVM support for dynamically typed languages. After its first release in Java 7, the invokedynamic opcode is used quite extensively by dynamic JVM-based languages like JRuby and even statically typed languages like Java.

In this tutorial, we're going to demystify invokedynamic and see how it canhelp library and language designers to implement many forms of dynamicity.

This method takes a String as the input and then performs the following steps:

Computing the input length (invokevirtual on length)

Comparing the length with the constant 3 (if_icmple and iconst_3)

Returning false if the length is less than or equal to 3

Interestingly, this is actually equivalent of the lambda we passed to the filter method:

c -> c.length() > 3

So instead of an anonymous inner class, Java creates a special static method and somehow invokes that method via invokedynamic.

Over the course of this article, we're going to see how this invocation works internally. But, first, let's define the problem that invokedynamic is trying to solve.

2.3. The Problem

Before Java 7, the JVM only had four method invocation types: invokevirtual to call normal class methods, invokestatic to call static methods, invokeinterface to call interface methods, and invokespecial to call constructors or private methods.

Despite their differences, all these invocations share one simple trait: They have a few predefined steps to complete each method call, and we can't enrich these steps with our custom behaviors.

There are two main workarounds for this limitation: One at compile-time and the other at runtime. The former is usually used by languages like Scala or Koltin and the latter is the solution of choice for JVM-based dynamic languages like JRuby.

The runtime approach is usually reflection-based and consequently, inefficient.

On the other hand, the compile-time solution is usually relying on code-generation at compile-time. This approach is more efficient at runtime. However, it's somewhat brittle and also may cause a slower startup time as there's more bytecode to process.

Now that we've got a better understanding of the problem, let's see how the solution works internally.

3. Under the Hood

invokedynamic lets us bootstrap the method invocation process in any way we want. That is, when the JVM sees an invokedynamic opcode for the first time, it calls a special method known as the bootstrap method to initialize the invocation process:

The bootstrap method is a normal piece of Java code that we've written to set up the invocation process. Therefore, it can contain any logic.

Once the bootstrap method completes normally, it should return an instance of CallSite. This CallSite encapsulates the following pieces of information:

A pointer to the actual logic that JVM should execute. This should be represented as a MethodHandle.

A condition representing the validity of the returned CallSite.

From now on, every time JVM sees this particular opcode again, it will skip the slow path and directly calls the underlying executable. Moreover, the JVM will continue to skip the slow path until the condition in the CallSite changes.

As opposed to the Reflection API, the JVM can completely see-through MethodHandles and will try to optimize them, hence the better performance.

Similar to all other bootstrap methods, this one takes at least three arguments as follows:

The Ljava/lang/invoke/MethodHandles$Lookup argument represents the lookup context for the invokedynamic

The Ljava/lang/String represents the method name in the call site – in this example, the method name is test

The Ljava/lang/invoke/MethodType is the dynamic method signature of the call site – in this case, it's ()Ljava/util/function/Predicate

In addition to these three arguments, bootstrap methods also can optionally accept one or more extra parameters. In this example, these are the extra ones:

The (Ljava/lang/Object;)Z is an erased method signature accepting an instance of Object and returning a boolean.

The REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z is the MethodHandle pointing to the actual lambda logic.

The (Ljava/lang/String;)Z is a non-erased method signature accepting one String and returning a boolean.

Put simply, the JVM will pass all the required information to the bootstrap method. Bootstrap method will, in turn, use that information to create an appropriate instance of Predicate. Then, the JVM will pass that instance to the filter method.

3.2. Different Types of CallSites

Once the JVM sees invokedynamic in this example for the first time, it calls the bootstrap method. As of writing this article, the lambda bootstrap method will use the InnerClassLambdaMetafactoryto generate an inner class for the lambda at runtime.

Then the bootstrap method encapsulates the generated inner class inside a special type of CallSite known as ConstantCallSite. This type of CallSite would never change after setup. Therefore, after the first setup for each lambda, the JVM will always use the fast path to directly call the lambda logic.

Although this is the most efficient type of invokedynamic, it's certainly not the only available option. As a matter of fact, Java provides MutableCallSiteand VolatileCallSiteto accommodate for more dynamic requirements.

3.3. Advantages

So, in order to implement lambda expressions, instead of creating anonymous inner classes at compile-time, Java creates them at runtime via invokedynamic.

One might argue against deferring inner class generation until runtime. However, the invokedynamic approach has a few advantages over the simple compile-time solution.

First, the JVM does not generate the inner class until the first use of lambda. Hence, we won't pay for the extra footprint associated with the inner class before the first lambda execution.

Additionally, much of the linkage logic is moved out from the bytecode to the bootstrap method. Therefore, the invokedynamic bytecode is usually much smaller than alternative solutions. The smaller bytecode can boost startup speed.

Suppose a newer version of Java comes with a more efficient bootstrap method implementation. Then our invokedynamic bytecode can take advantage of this improvement without recompiling. This way we can achieve some sort of forwarding binary compatibility. Basically, we can switch between different strategies without recompilation.

Finally, writing the bootstrap and linkage logic in Java is usually easier than traversing an AST to generate a complex piece of bytecode. So, invokedynamic can be (subjectively) less brittle.

4. More Examples

Lambda expressions are not the only feature, and Java is not certainly the only language using invokedynamic. In this section, we're going to get familiar with a few other examples of dynamic invocation.

4.1. Java 14: Records

Records are a new preview feature in Java 14 providing a nice concise syntax to declare classes that are supposed to be dumb data holders.

4.2. Java 9: String Concatenation

Prior to Java 9, non-trivial string concatenations were implemented using StringBuilder. As part of JEP 280, string concatenation is now using invokedynamic. For instance, let's concatenate a constant string with a random variable: