Indy deep dive

Nuno Caro
9 min readApr 1, 2021
Photo by Artem Sapegin on Unsplash

In today’s post, I’ll talk about something that always interested me, but never had the time to perform a deep dive on, that is invokedynamic bytecode and boostrap methods (BSM) in the JVM.

Life before indy

As you all are aware by this point, up until Java 1.7, the JVM was an ecosystem mainly built around static method resolution, which should not be confused with method dispatch. JVM has always had what is called single-dispatch polymorphism.

When the program was compiled, the bytecode emitted would include an invokeX bytecode with a direct reference to the target method (MethodRef structure), and this is what is called static resolution, because is bound at compile time.

This means that languages with multi-dispatch methods are hard to model in this world, such as Groovy. You might also be wondering about method overloads, but they are bound at compile time also.

There are ways to emulate multi-dispatch in Java with patterns like Visitor, but it generates a nasty cyclic dependency in your code. Won’t talk about acyclic visitors because it would be a spoiler 😊.

Enter indy, the bytecode we all needed, but never knew it. This new bytecode does not point to a method reference, but to a structure that holds code to produce a method reference. This tiny change allows user code to be run and find the target method at runtime, instead of at compile time. This is called a bootstrap method (BSM).

The BSM must adhere to very few restrictions to be considered valid, which I’ll talk about later, but it returns an instance of CallSite, which contains the target method to call.

What is the difference to other invoke types?

Prior to indy, we had 4 different kinds of invoke bytecodes

  1. invokestatic
  2. invokevirtual
  3. invokeinterface
  4. invokespecial

Not going to explain each one of them, but assume all implement different kinds of invocation logic.

All the code we used is in GitHub and we will use javap tool to look at the generated bytecodes:

Fig 2. Bytecode of the example with static resolution

The above example is just a simple function that prints to the console the square of a random int. In this excerpt, we can see all the old invoke types in action. We can see that all of them point to a structure called Method (more into this). The rest of the bytecode is just stack manipulation.

Now I’ll expose the exact same example, but only using invokedynamic instructions, which in this case are obtained by using lambda expressions:

Fig 3. Bytecode of the example with dynamic resolution

A bit more cumbersome to understand, but stay with me. First of all, we can see that we are dealing with a different structure, InvokeDynamic, which contains 2 arguments (in the comments), an index, and a signature.

Let’s skip the signature for now, because it is just mainly for validations, and focus on the first argument. This argument is an index to the classe’s bootstrap methods table.

The bootstrap method (BSM)

The BSM is the mechanism responsible to resolve the actual method to call. Up until this point, the JVM has no idea what will be the target of this invoke instruction. The cherry on top is that only part of the signature of every BSM is specified, meaning that this is actually user logic being called (see the dynamic nature already).

The actual specification of the BSM is something like this:

  • The first parameter is a Lookup instance
  • The second parameter is the method name
  • The third parameter is the expected type descriptor for the method signature, encoded in a MethodType instance.
  • Additional parameters can be specified, as long as they can be encoded in the constant pool as well
  • The return type is CallSite

I’m omitting some details, due to edge cases and condy, but I’ll mention it later.

Fig 4. Bootstrap method table for Fig2

In this example, we can see that every bootstrap method receives 3 additional parameters, which are also related to the actual implementation details of the lambda project in Java.

Backing up a bit to the example in Fig2, now we can understand that the invokedynamic bytecodes just create instances of the interfaces from the lambda expressions, which are used downstream to produce the expected result. These last invocations are all invokeinterface, so it makes sense 😊.

Lookup, MethodHandle, and CallSite

But wait, what about all these parameters? They need to be very closely related to the JVM implementation for it to work right?

This is totally correct. If we navigate the source code of Lookup and MethodHandle, they will delegate to native code, meaning they are boundary classes that expose VM services in some way.

CallSite

Broadly speaking, a CallSite is a holder for a MethodHandle. There are several types of call sites, but most of them are constant, meaning they hold the same MethodHandle for all it’s lifetime.

MethodHandle

What are they? First, the MethodHandle is a structure that wraps around a function execution, very similar to a native function pointer, but on steroids. It is possible to manipulate parameters, return values, and more as we do on functional composition patterns.

This class provides 2 main methods, invoke and invokeExact. Both have the same signature but behave differently, but what I’d like to call to your attention is that both are marked with PolimorphicSignature. This tells the compiler to emit bytecode in a different way.

This is better explained in code:

The first call, in line 26, despite being compatible with the method signature will fail at runtime because the result type is not the same as defined in the method. A good IDE will flag bad usages of MethodHandles when it can.

Fig 6. Bytecode for invoking varargs with and without PolymorphicSignature

The bytecode emitted on calls like this is not the same as you’d expect from a varargs method. As we can see in #10, the signature used is different from #14. It generates an invoke call with a signature based on the types that are on the call site, and not on the actual method’s signature.

This is the missing piece of the puzzle for dynamic callsite resolution. I actually think this is close to black magic, because the compiler emits different method signatures based on an internal annotation, but hey, at least is very well documented and can’t be used externally.

Lookup

Finally, the Lookup object is kind of like a GPS beacon of where you’d like to start your finds from. It records the exact spot where it was created to allow anyone with access to it to find methods that are accessible in that exact spot.

For instance, if a lookup object is created inside a class, it has access to find any private member of that class, effectively exposing it’s internals.

In the early days, this class only allowed to find methods and it’s variants (constructors, getters, setters), but now it enables to find pretty much anything, including VarHandles and also define new classes.

In our use case, it is passed to the BSM as context to find the actual method we want. We may choose not to use it.

Reflection VS Indy

One doubt that I had when this was released was, is this like reflection? And the answer I have is: not quite, but they touch on some points.

The main difference is that, to use indy you need to know what you are looking for.

Reflection has its own, and very valid, use case. Nowadays it is used as the stepping stone for many established frameworks, such as Spring, Hibernate, Jakarta EE, anything that relies on automatic discovery after the JVM is already running.

It’s true both allow us to invoke methods on objects we don’t exactly know at compile-time, but seems a stretch to call them competitors.

But we are in the middle of a change in paradigm. As the world is changing to SoA, with boundaries usually defined in non binary formats like WSDL, GRPC, or OpenApi, application developers start to work in a closed world assumption giving birth to many more compile-time frameworks like Quarkus or Micronaut, or Graal. These kinds of tools usually generate all its code at compile-time, but we may see some use cases where dynamic resolution is a better fit.

Other indy improvements

Java 8 Lambda

As you are certainly aware, the lambda implementation in Java actually was built on top of all this machinery.

If we dive into the bytecode generated for lambdas, we will see invokedynamic everywhere and BSMs that use a class called LambdaMetafactory.

This class is responsible to generate anonymous classes at runtime that implement all the lambda interfaces and capture the available scope.

This class itself would be an article on its own with its scope capturing mechanics and the lambda forms, but it falls out of scope of this post.

Just keep in mind that without indy, we probably didn’t have lambdas so smoothly integrated in Java. We would probably have something closer to real function types, so maybe a missed opportunity here?

Java 9+ improvements

There were other use cases in the JDK where indy was used besides lambdas.

Before Java 9 string concatenation was done by creating an instance of StringBuilder and appending the bits one after the other. With indy it delegates to a factory that performs substitution against a single string constant.

Another example is with Java 15 records. The equals and hashCode methods for each record do not exist until its first usage. It is an indy call to ObjectMethods class in order to generate them.

New language implementations

The main use case for indy has always been to enable dynamic method resolution on the JVM, but it seems a bit odd to use it on static resolution languages like Java. Where it really shines is in languages with dynamic nature.

After indy debuted, there have been two major refactors on existing languages to take advantage of, one in Groovy and the other in JRuby. This blog post gives a taste of what was like before and after indy from the perspective of the JRuby maintainers.

New languages compiled with the Truffle API on GraalVM will most likely be using indy in some way, since it already supports Javascript, Python and Ruby, but I haven’t played around with it so don’t take my word for it.

Condy

To wrap things up I would like to mention another concept that may have slipped your radar which is ConstantDynamic (condy for short).

Condy is a lot like indy but for constants. It is the ldc bytecode with the same BSM mechanics and enables all sorts of new constant types in the constant pool.

It provides a new structure named Dynamic that is able to store pretty much anything. It is hard to explain the support structures, but it basically allows storing arbitrary objects.

A cool feature related to this already released in JDK11, which is the ConstantBootstraps class. This class already contains BSM for the most common use cases, which include static final fields.

But why should we care? Condy will pave the way for 2 main improvements. The first is a truly unmodifiable root for JITs to start optimization because remember that final doesn’t always mean constant in Java.

The second and most interesting feature is related to compilation. If we can have an API written in such a way that the compiler can fold all it’s operations into a single constant it can generate then a single ldc instruction with the result of that computation.

With condy the BSM requirements change a bit, but is still pretty similar to indy:

  • The first parameter is a Lookup instance
  • The second parameter is the method name
  • The third parameter is a type descriptor of the returned object.
  • Additional parameters can be specified, as long as they can be encoded in the constant pool as well
  • The return type is Object but must match the type descriptor

Conclusion

Despite the fact that we don’t use these features directly, we are already benefiting from their existence in one way or another, and they will become the stepping stone for better things to come.

I hope this post has been useful to better understand what goes on behind the covers of a machine like JVM.

--

--

Nuno Caro

Computer science geek with a splash of party animal.