GraalVM became one of the most popular topics in the JVM ecosystem. It promises the highest possible speed of running JVM-based programs (when compiled to native images), hand in hand with the smaller memory footprint. It sounds interesting enough to give it a try. And today we are going to play around a little bit with running simple Groovy program after compiling to a standalone native image.

Introduction

This blog post documents pretty simple use case of running Groovy code compiled to a GraalVM native image. It will give you a good understanding how to start and how to solve the problems you will most probably face when you start playing around with your own examples.

Prerequisites

We will be using following tools:

  • GraalVM 1.0.0-RC7 (the latest version while writing this blog post)

  • Groovy 2.5.2

The easiest way to install GraalVM and Groovy is to use SDKMAN! command line tool.

Let’s code

For the purpose of this experiment we are going to use a simple Groovy program that sums and multiplies numbers:

Listing 1. RandomNumber.groovy
class RandomNumber {
    static void main(String[] args) {
        def random = new Random().nextInt(1000)

        println "The random number is: $random"

        def sum = (0..random).sum { int num -> num * 2 }

        println "The doubled sum of numbers between 0 and $random is $sum"
    }
}

GraalVM prefers static compilation[1] and that is why we are going to create compiler configuration file like:

Listing 2. compiler.groovy
withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
    ast(groovy.transform.TypeChecked)
}
Alternatively you could annotate the class with @groovy.transform.CompileStatic and @groovy.transform.TypeChecked.

We are ready to compile our code with groovyc compiler:

Listing 3. Compiling Groovy code
groovyc --configscript compiler.groovy RandomNumber.groovy

Code compiled. Let’s make sure we are using a correct GraalVM JDK:

Listing 4. Checking Java version
java -version

openjdk version "1.8.0_172"
OpenJDK Runtime Environment (build 1.8.0_172-20180625212755.graaluser.jdk8u-src-tar-g-b11)
GraalVM 1.0.0-rc7 (build 25.71-b01-internal-jvmci-0.48, mixed mode)

Everything is ready. Let’s run the code using GraalVM JDK:

Listing 5. Running Java program with GraalVM
java -cp ".:$HOME/.m2/repository/org/codehaus/groovy/groovy/2.5.2/groovy-2.5.2.jar" RandomNumber
The random number is: 876
The doubled sum of numbers between 0 and 876 is 768252
Running Groovy compiled code as a Java program requires adding org.codehaus.groovy:groovy:2.5.2:jar to the classpath. In this example I am using JAR file from my local Maven repository.

Creating native image

Running our example inside the JVM was nice, but GraalVM offers much more. We can create standalone native image that will consume much less memory and will execute in a blink of an eye. Let’s give it a try:

Listing 6. Building native image with GraalVM
native-image -H:+ReportUnsupportedElementsAtRuntime \
        -cp ".:$HOME/.m2/repository/org/codehaus/groovy/groovy/2.5.2/groovy-2.5.2.jar" \
        --no-server \
        RandomNumber

Running the command will produce a following output:

[randomnumber:13187]    classlist:   2,144.27 ms
[randomnumber:13187]        (cap):     662.01 ms
[randomnumber:13187]        setup:   1,245.07 ms
[randomnumber:13187]   (typeflow):   7,048.82 ms
[randomnumber:13187]    (objects):   6,547.35 ms
[randomnumber:13187]   (features):     119.57 ms
[randomnumber:13187]     analysis:  15,433.33 ms
[randomnumber:13187]     universe:     564.07 ms
[randomnumber:13187]      (parse):   1,903.46 ms
[randomnumber:13187]     (inline):   2,529.37 ms
[randomnumber:13187]    (compile):  11,180.52 ms
[randomnumber:13187]      compile:  19,392.95 ms
[randomnumber:13187]        image:   1,872.31 ms
[randomnumber:13187]        write:     373.53 ms
[randomnumber:13187]      [total]:  42,717.44 ms

Let’s run the program then:

./randomnumber
The random number is: 139
Exception in thread "main" groovy.lang.MissingMethodException: No signature of method: RandomNumber$_main_closure1.doCall() is applicable for argument types: (Integer) values: [0]
Possible solutions: findAll(), findAll(), isCase(java.lang.Object), isCase(java.lang.Object)
	at java.lang.Throwable.<init>(Throwable.java:250)
	at java.lang.Exception.<init>(Exception.java:54)
	at java.lang.RuntimeException.<init>(RuntimeException.java:51)
	at groovy.lang.GroovyRuntimeException.<init>(GroovyRuntimeException.java:33)
	at groovy.lang.MissingMethodException.<init>(MissingMethodException.java:49)
	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:256)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at groovy.lang.Closure.call(Closure.java:421)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6613)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6513)
	at RandomNumber.main(RandomNumber.groovy:8)
	at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:163)

Something went wrong. The first print The random number is: 139 was shown in the console, but executing sum operation with a closure failed with the exception. The reason of this is because GraalVM uses AOT (ahead of time) compilation, which comes with some limitations (e.g. when it comes to Java reflection). The good news is that GraalVM allows us to configure manually which classes are loaded via reflection, so GraalVM will be ready to do so. Let’s create a file called reflection.json with the following content:

Listing 7. reflection.json
[
  {
    "name": "RandomNumber$_main_closure1",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  }
]
More about manual reflection configuration can be found here.

Let’s run native-image again, but this time with -H:ReflectionConfigurationFiles=reflection.json parameter added:

Listing 8. Building native image with GraalVM
native-image -H:+ReportUnsupportedElementsAtRuntime \
        -H:ReflectionConfigurationFiles=reflection.json \
        -cp ".:$HOME/.m2/repository/org/codehaus/groovy/groovy/2.5.2/groovy-2.5.2.jar" \
        --no-server \
        RandomNumber

When we run ./randomnumber now, we will something like this in the console:

The random number is: 673
java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$519
	at java.lang.Throwable.<init>(Throwable.java:287)
	at java.lang.Exception.<init>(Exception.java:84)
	at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:75)
	at java.lang.ClassNotFoundException.<init>(ClassNotFoundException.java:82)
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
	at com.oracle.svm.core.jdk.Target_java_lang_ClassLoader.loadClass(Target_java_lang_ClassLoader.java:126)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.createProxy(GeneratedMetaMethod.java:101)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.proxy(GeneratedMetaMethod.java:93)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.isValidMethod(GeneratedMetaMethod.java:78)
	at groovy.lang.MetaClassImpl.chooseMethodInternal(MetaClassImpl.java:3232)
	at groovy.lang.MetaClassImpl.chooseMethod(MetaClassImpl.java:3194)
	at groovy.lang.MetaClassImpl.getNormalMethodWithCaching(MetaClassImpl.java:1402)
	at groovy.lang.MetaClassImpl.getMethodWithCaching(MetaClassImpl.java:1317)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1087)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6620)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6513)
	at RandomNumber.main(RandomNumber.groovy:8)
	at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:163)
Exception in thread "main" groovy.lang.GroovyRuntimeException: Failed to create DGM method proxy : java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$519
	at java.lang.Throwable.<init>(Throwable.java:287)
	at java.lang.Exception.<init>(Exception.java:84)
	at java.lang.RuntimeException.<init>(RuntimeException.java:80)
	at groovy.lang.GroovyRuntimeException.<init>(GroovyRuntimeException.java:46)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.createProxy(GeneratedMetaMethod.java:106)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.proxy(GeneratedMetaMethod.java:93)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.isValidMethod(GeneratedMetaMethod.java:78)
	at groovy.lang.MetaClassImpl.chooseMethodInternal(MetaClassImpl.java:3232)
	at groovy.lang.MetaClassImpl.chooseMethod(MetaClassImpl.java:3194)
	at groovy.lang.MetaClassImpl.getNormalMethodWithCaching(MetaClassImpl.java:1402)
	at groovy.lang.MetaClassImpl.getMethodWithCaching(MetaClassImpl.java:1317)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1087)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6620)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6513)
	at RandomNumber.main(RandomNumber.groovy:8)
	at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:163)
Caused by: java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$519
	at java.lang.Throwable.<init>(Throwable.java:287)
	at java.lang.Exception.<init>(Exception.java:84)
	at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:75)
	at java.lang.ClassNotFoundException.<init>(ClassNotFoundException.java:82)
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
	at com.oracle.svm.core.jdk.Target_java_lang_ClassLoader.loadClass(Target_java_lang_ClassLoader.java:126)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.createProxy(GeneratedMetaMethod.java:101)
	... 12 more

This time class org.codehaus.groovy.runtime.dgm$519 cannot be found. These dgm$…​ classes are Groovy classes representing generate meta methods. Let’s add it to the reflection.json and repeat the last two steps. It will fail one more time - this time class org.codehaus.groovy.runtime.dgm$1172 cannot be found. Let’s add it and repeat. Final reflection.json file should look like this:

Listing 9. reflection.json
[
  {
    "name": "RandomNumber$_main_closure1",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  },
  {
    "name": "org.codehaus.groovy.runtime.dgm$519",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  },
  {
    "name": "org.codehaus.groovy.runtime.dgm$1172",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  }
]

And now when we try to run ./randomnumber we will see the following output:

The random number is: 161
The doubled sum of numbers between 0 and 161 is 26082

It worked, finally! In this case we only had to add these 3 classes to reflection configuration. When you run your own example you may have to add even more before your program executes as expected.

Let’s compare execution times

After building and running standalone executable it is a good time to make a short comparison. We are not going to do a detailed benchmark - we just want to test the cold start of the program in 3 different variants.

1: Running RandomNumber.groovy with a groovy command line (1,03s):

time groovy RandomNumber

The random number is: 546
The doubled sum of numbers between 0 and 546 is 298662

groovy RandomNumber  1,03s user 0,06s system 192% cpu 0,567 total

2: Running compiled Groovy code with GraalVM JVM (0,50s):

time java -cp ".:$HOME/.m2/repository/org/codehaus/groovy/groovy/2.5.2/groovy-2.5.2.jar" RandomNumber

The random number is: 437
The doubled sum of numbers between 0 and 437 is 191406

java -cp  RandomNumber  0,50s user 0,04s system 194% cpu 0,274 total

3: Running standalone native image (0,00s):

time ./randomnumber

The random number is: 675
The doubled sum of numbers between 0 and 675 is 456300

./randomnumber  0,00s user 0,00s system 92% cpu 0,007 total

That’s amazing! I wouldn’t thought that Java program can execute in a blink of an eye. And here you can see what does it look like in action:

Limitations

I must say that not everything look so bright. You have to be aware of many limitations you will face when you start building Groovy native images with GraalVM:

  1. Building native images from dynamic Groovy scripts does not work at the moment[2].

  2. Dynamic runtime metaprogramming may not work at all in GraalVM (some parts may be fixed by configuring classes for AOT reflection).

  3. Closures require manual configuration for reflection and you will face some issues when trying to cast a closure to some other type (e.g. when you use a closure in place of a functional interface).

  4. Grape, one of the most valuable Groovy scripts feature won’t work as standalone native image, because it requires Groovy command line tool and its class loader that understand what does @Grab annotation mean.

  5. And last but not least - Groovy native image for this example weight 24 MB, which is quite a lot comparing to what this application does.

An example

Before we close this article, let’s take a look at example that does not work with GraalVM. Let’s refactor above example to use Java 8 Stream API and closures in place of lambda expressions:

Listing 10. RandomNumber.groovy
import groovy.transform.CompileStatic
import groovy.transform.TypeChecked

import java.util.stream.IntStream

@CompileStatic
@TypeChecked
class RandomNumber {
    static void main(String[] args) {
        def random = new Random().nextInt(1000)

        println "The random number is: $random"

        Long sum = IntStream.rangeClosed(0, random)
                    .boxed()
                    .map { it * 2 }
                    .mapToLong { it -> (long) it }
                    .sum()

        println "The doubled sum of numbers between 0 and $random is $sum"
    }
}

It compiles, GraalVM JDK runs it on JVM, native image builds. But when we try to run it we will see following output:

The random number is: 226
Exception in thread "main" org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'RandomNumber$_main_closure1@7fcfe0745d78' with class 'RandomNumber$_main_closure1' to class 'java.util.function.Function'
	at java.lang.Throwable.<init>(Throwable.java:265)
	at java.lang.Exception.<init>(Exception.java:66)
	at java.lang.RuntimeException.<init>(RuntimeException.java:62)
	at java.lang.ClassCastException.<init>(ClassCastException.java:58)
	at org.codehaus.groovy.runtime.typehandling.GroovyCastException.<init>(GroovyCastException.java:40)
	at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.continueCastOnSAM(DefaultTypeTransformation.java:414)
	at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.continueCastOnNumber(DefaultTypeTransformation.java:328)
	at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.castToType(DefaultTypeTransformation.java:242)
	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.castToType(ScriptBytecodeAdapter.java:617)
	at RandomNumber.main(RandomNumber.groovy:15)
	at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:163)

As you can see closure we used for map operation cannot be cast to java.util.function.Function and program terminates. This is a huge problem for many Groovy programs - we tend to use closure in place of other types and we expect correct coercion to happen. I’m guessing this example requires some effort and finding which classes should be configured manually for reflection. I will share an update when I find solution to that problem.

Conclusion

I hope you have learned something useful from this blog post. I will continue exploring the world of GraalVM in cooperation with different technologies. I’m looking forward for learning and experimenting with more real-life and useful examples. I strongly encourage you to keep an eye on GraalVM - it is one of the hottest JVM topics these days for a good reason. And if you are looking for a project that is experimenting actively with GraalVM, take a look at Micronaut framework - people from OCI did a great job in this area and they documented their efforts in an official Micronaut user guide.

Szymon Stepniak

Upwork's Top Rated freelancer, Toruń Java User Group founder, open source contributor, Stack Overflow active user, bedroom guitar player.