Skip to the content.
blog talks about resources

invokedynamic in GraalVM native image: how is it possible?

GraalVM has this really cool feature: you can build a native executable from your Java, Scala, or other application. Your application becomes lighter, starts super quickly, it’s more performant, and you don’t need a JVM to run it on your machine.

How is it possible to achieve such results?

When we run a Java application on the JVM, a lot of things are happening at runtime. For example, a lot of dependencies are there just for the reason that the app doesn’t know in advance what it will need. The execution engine of JVM is basically interpreting the JVM bytecode line by line, at the same time performing JIT compilation here and there to make it faster.

However, when we build a native image, we are compiling the bytecode into machine code immediately, before the runtime even happens. This process is referred to as Ahead Of Time (AOT) compilation. To compile the bytecode ahead of time into machine code, we need to be able to look at it and know for sure which types we are going to deal with (thankfully, Java is a statically typed language). Also, we need to know exactly which methods will be called when the program runs.

When GraalVM builds the native image, it performs a very important step called static analysis. It takes the entrypoint methods and goes from there down the tree of the possible calls to find all reachable types, fields, and methods. These are then included in a graph, where all elements are connected and, if put in isolation, are enough for our program to run successfully. Anything that is not reachable is thrown away.

How does it deal with dynamic stuff?

Great, you will say, what about reflection?

GraalVM is smart about it. If it’s possible to determine in advance which class/method we’re looking for, they are going to be included in our closed world of reachable elements, and everything will work. If it’s impossible to determine (for example, when the class name comes from user input at runtime), we can make configurations for building the native image, where we can list all classes/methods we’re going to need for reflection.

There are more things that can be configured similarly: dynamic proxy, JNI, serialization… Everything else is not dynamic in Java, right?

…r-right?

Native image limitations

There is a section in GraalVM documentation that mentions some limitations of the Closed-World optimization. For example, it states that invokedynamic and Method Handles are not supported, with a very important remark, however:

Note that invokedynamic use cases generated by javac for, e.g., Java lambda expressions and string concatenation are supported because they do not change called methods at image run time.

invokedynamic, also known as indy, has been designed as a bytecode instruction to support dynamically-typed languages that started emerging on top of the JVM. In other words, it is literally designed to mess around at runtime: some new methods can be generated and invoked, and so on.

These days the javac compiler uses it too! Specifically for some cases of String concatenation, for lambda expressions, and for the newest pattern matching for switch. I have explained the indy + pattern matching combo in detail in my previous article if you are curious.

indy in String concatenation

Let’s consider a specific example. Indy is used for String concatenation when there is a dynamic element in it (non-constant value).

So, if it’s just System.out.println(“test” + 1);, the javac won’t use indy for it. Instead, the javac compiler will create a single constant, “test1”, and put it into the constant pool section. Afterward, it will simply use it as an argument of the println method.

However, for the following code we will encounter the invokedynamic instruction:

int a = 1;
System.out.println("test" + a);

For this case, we will see this bootstrap method referenced in the bytecode:

BootstrapMethods:
    0: #32 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:
            #38 test\u0001

When the makeConcatWithConstants bootstrap method is invoked at runtime, it goes through a logic labyrinth to find and adjust the target MethodHandle, which basically depends on the arguments. So, it looks up at runtime, which method to invoke next.

However, in this case, it is possible to look at the bytecode, particularly at the bootstrap method arguments and the state of the operand stack when indy is used, and know in advance which method it would invoke at runtime.

indy + native image = ?

Technically, invokedynamic together with bootstrap methods makes it possible to choose what method to call at runtime based on user input or the weather. It is possible to confuse the GraalVM native image builder, but javac is nice enough to not do this. Yet :)

javac generates the bytecode, where indy can be interpreted only one way. Same as String concatenation, lambdas and pattern matching bring us to specific target methods, which are determinable ahead of time.

That works perfectly fine for GraalVM native image, and no extra configurations are necessary.

Inspecting the native image

It’s possible to see which classes and methods have been included in the “closed world” when the image was built.

Let’s say we have a Test class having String concatenation logic which involves indy and bootstrap methods:

public class Test {
    public static void main(String[] args) {
        int a = 1;
        System.out.println("test" + a);
    }
}

Let’s compile it and build a native image:

$ javac Test.java
$ native-image Test

Now we can use a native-image-inspect tool to gather information about everything that comes into the closed world. Let’s also output it into some log file:

$ native-image-inspect test | jq > log.txt

We do know that the bootstrap method makeConcatWithConstants must be invoked. We also know that this invocation is handled by the invoke method in the JVM when it encounters the invokedynamic instruction. However, if we search the log file that we’ve generated, we won’t find them there.

Now comes the fun part. The bootstrap method serves the purpose of finding the target method to invoke, and the appropriate target method is present in the log:

{
    "declaringClass": "java.lang.StringConcatHelper",
    "name": "simpleConcat",
    "parameterTypes": [
        "java.lang.Object",
        "java.lang.Object"
    ]
},

So what is going on?

GraalVM AOT compiler makes use of the JVM Compiler Interface (JVMCI), which is also part of OpenJDK these days. This interface makes it possible for the compiler written in Java to interact with the very low level of JVM, and actually it provides our Java compiler (graal) the translations from the bytecode into the machine code. Using JVMCI, we can tell JVM that hey, we are your new compiler now, please forget about all that JIT stuff that you usually do.

When the native image is being built, one of the first steps is static analysis. During this analysis, we have to make sure that all dynamic elements are known and no longer dynamic. So we go to JVMCI and ask it to resolve the entry referenced for invokedynamic in the constant pool, which leads to the bootstrap method invocation at image build time.

Then, all the frame states are calculated, and the methods are inlined as far as possible. In the end, if there’s only one invocation left, the invokedynamic is replaced with the static invocation of a target method. If there’s more than one invocation possible after all the bootstrapping, the native image generator will tell us that it’s not going to work.

Conclusion

This invokedynamic & Method Handles example is just one demonstration of how smartly the GraalVM native image generator can handle dynamic-ish behaviours of our application. Basic cases of reflection etc. wouldn’t be a problem either.

Of course, the truly dynamic behaviours need extra configuration. I assume that at some point, there will be configuration options available for invokedynamic with Method Handles as well.

back to blog
Hits