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 can help library and language designers to implement many forms of dynamicity.
2. Meet Invoke Dynamic
Let's start with a simple chain of Stream API calls:
public class Main { public static void main(String[] args) { long lengthyColors = List.of("Red", "Green", "Blue") .stream().filter(c -> c.length() > 3).count(); } }
At first, we might think that Java creates an anonymous inner class deriving from Predicate and then passes that instance to the filter method. But, we'd be wrong.
2.1. The Bytecode
To check this assumption, we can take a peek at the generated bytecode:
javap -c -p Main // truncated // class names are simplified for the sake of brevity // for instance, Stream is actually java/util/stream/Stream 0: ldc #7 // String Red 2: ldc #9 // String Green 4: ldc #11 // String Blue 6: invokestatic #13 // InterfaceMethod List.of:(LObject;LObject;)LList; 9: invokeinterface #19, 1 // InterfaceMethod List.stream:()LStream; 14: invokedynamic #23, 0 // InvokeDynamic #0:test:()LPredicate; 19: invokeinterface #27, 2 // InterfaceMethod Stream.filter:(LPredicate;)LStream; 24: invokeinterface #33, 1 // InterfaceMethod Stream.count:()J 29: lstore_1 30: return
Despite what we thought, there's no anonymous inner class and certainly, nobody is passing an instance of such a class to the filter method.
Surprisingly, the invokedynamic instruction is somehow responsible for creating the Predicate instance.
2.2. Lambda Specific Methods
Additionally, the Java compiler also generated the following funny-looking static method:
private static boolean lambda$main$0(java.lang.String); Code: 0: aload_0 1: invokevirtual #37 // Method java/lang/String.length:()I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn
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.
3.1. Bootstrap Method Table
Let's take another look at the generated invokedynamic bytecode:
14: invokedynamic #23, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
This means that this particular instruction should call the first bootstrap method (#0 part) from the bootstrap method table. Also, it mentions some of the arguments to pass to the bootstrap method:
- The test is the only abstract method in the Predicate
- The ()Ljava/util/function/Predicate represents a method signature in the JVM – the method takes nothing as input and returns an instance of the Predicate interface
In order to see the bootstrap method table for the lambda example, we should pass -v option to javap:
javap -c -p -v Main // truncated // added new lines for brevity BootstrapMethods: 0: #55 REF_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: #62 (Ljava/lang/Object;)Z #64 REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z #67 (Ljava/lang/String;)Z
The bootstrap method for all lambdas is the metafactory static method in the LambdaMetafactory class.
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 InnerClassLambdaMetafactory to 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 MutableCallSite and VolatileCallSite to 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 forward 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.
Here's a simple record example:
public record Color(String name, int code) {}
Given this simple one-liner, Java compiler generates appropriate implementations for accessor methods, toString, equals, and hashcode.
In order to implement toString, equals, or hashcode, Java is using invokedynamic. For instance, the bytecode for equals is as follows:
public final boolean equals(java.lang.Object); Code: 0: aload_0 1: aload_1 2: invokedynamic #27, 0 // InvokeDynamic #0:equals:(LColor;Ljava/lang/Object;)Z 7: ireturn
The alternative solution is to find all record fields and generate the equals logic based on those fields at compile-time. The more we have fields, the lengthier the bytecode.
On the contrary, Java calls a bootstrap method to link the appropriate implementation at runtime. Therefore, the bytecode length would remain constant regardless of the number of fields.
Looking more closely at the bytecode shows that the bootstrap method is ObjectMethods#bootstrap:
BootstrapMethods: 0: #42 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap: (Ljava/lang/invoke/MethodHandles$Lookup; Ljava/lang/String; Ljava/lang/invoke/TypeDescriptor; Ljava/lang/Class; Ljava/lang/String; [Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object; Method arguments: #8 Color #49 name;code #51 REF_getField Color.name:Ljava/lang/String; #52 REF_getField Color.code:I
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:
"random-" + ThreadLocalRandom.current().nextInt();
Here's how the bytecode looks like for this example:
0: invokestatic #7 // Method ThreadLocalRandom.current:()LThreadLocalRandom; 3: invokevirtual #13 // Method ThreadLocalRandom.nextInt:()I 6: invokedynamic #17, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)LString;
Moreover, the bootstrap methods for string concatenations are residing in the StringConcatFactory class:
BootstrapMethods: 0: #30 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants: (Ljava/lang/invoke/MethodHandles$Lookup; Ljava/lang/String; Ljava/lang/invoke/MethodType; Ljava/lang/String; [Ljava/lang/Object;)Ljava/lang/invoke/CallSite; Method arguments: #36 random-\u0001
5. Conclusion
In this article, first, we got familiar with the problems the indy is trying to solve.
Then, by walking through a simple lambda expression example, we saw how invokedynamic works internally.
Finally, we enumerated a few other examples of indy in recent versions of Java.