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 19.0.0 (the latest version while writing this blog post)

  • Groovy 2.5.7

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

Let’s make sure we are using correct GraalVM Java distribution.

Listing 1. Checking Java version
$ java -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-20190420092731.buildslave.jdk8u-src-tar--b03)
OpenJDK GraalVM CE 19.0.0 (build 25.212-b03-jvmci-19-b01, mixed mode)

Starting from 19.0.0 version, GraalVM does not contain native-image tool as a default part of the distribution, so we need to install it using GraalVM Component Updater.

Listing 2. Installing native-image using gu (a tool shipped with GraalVM distribution)
$ gu install native-image

Downloading: Component catalog from www.graalvm.org
Processing component archive: Native Image
Downloading: Component native-image: Native Image  from github.com
Installing new component: Native Image licence files (org.graalvm.native-image, version 19.0.0)

Let’s check if we use a proper version of Groovy.

Listing 3. Checking Groovy version
$ groovy -version
Groovy Version: 2.5.7 JVM: 1.8.0_212 Vendor: Oracle Corporation OS: Linux

Let’s also check if GROOVY_HOME variable is set correctly. (If you installed Groovy using SDKMAN!, the environment variable should be set correctly.)

Listing 4. Checking if GROOVY_HOME environment variable is set
$ env | grep GROOVY_HOME
GROOVY_HOME=/home/wololock/.sdkman/candidates/groovy/current

We are all set and ready to code!

An example

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

Listing 5. 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], thus we need to switch Groovy from dynamic to a static compilation. We can simply put @groovy.transform.CompileStatic annotation over the class definition in RandomNumber.groovy file, or we can create a compiler configuration file that adds this annotation to all classes.

Listing 6. Compiler configuration file compiler.groovy
withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
}

We are ready to compile our code with groovyc compiler:

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

The code compiled. We can now test if our Groovy script runs as a Java program.

Listing 8. Running Java program with GraalVM
$ java -cp ".:$GROOVY_HOME/lib/groovy-2.5.7.jar" RandomNumber
The random number is: 313
The doubled sum of numbers between 0 and 313 is 98282

It run smoothly! You have probably noticed a small lag - the result was not printed instantly to the console. We can run the program again, but this time with the time command added to measure execution time.

Listing 9. Measuring Java program execution time
$ time java -cp ".:$GROOVY_HOME/lib/groovy-2.5.7.jar" RandomNumber
The random number is: 859
The doubled sum of numbers between 0 and 859 is 738740

real	0m0,306s
user	0m0,692s
sys	0m0,073s

It took 306 ms to complete. Is it slow or fast? It depends. If we compare it to the same program, but executed inside Groovy interpreter command line tool, the Java program is approximately 2.5 times faster.

$ time groovy RandomNumber.groovy
The random number is: 711
The doubled sum of numbers between 0 and 711 is 506232

real	0m0,885s
user	0m2,060s
sys	0m0,183s

Let’s see if GraalVM’s native image can do better than that.

Creating native image

One of the most interesting features of GraalVM is its ability to create standalone native binary file from given Java bytecode (either Java .class or .jar files).

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 10. Building native image with GraalVM
$ native-image --allow-incomplete-classpath \(1)
    --report-unsupported-elements-at-runtime \(2)
    --initialize-at-build-time \(3)
    --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \(4)
    --no-fallback \(5)
    --no-server \(6)
    -cp ".:$GROOVY_HOME/lib/groovy-2.5.7.jar" \(7)
    RandomNumber(8)

As you can see there are many parameters passed to the native-image command. We use to allow image building with an incomplete classpath. If we didn’t allow that, native image compilation would fail with the error like the one below.

Listing 11. Compilation error thrown when --allow-incomplete-classpath parameter is missing
Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved method during parsing: org.codehaus.groovy.control.XStreamUtils.serialize(java.lang.String, java.lang.Object). To diagnose the issue you can use the --allow-incomplete-classpath option. The missing method is then reported at run time when it is accessed the first time.

The second parameter makes usages of unsupported methods and fields to be reported at a runtime (when they are accessed the first time) instead of the build time. It is also critical to our case. Without this parameter set, compilation fails with the following error.

Listing 12. Compilation error thrown when --report-unsupported-elements-at-runtime parameter is missing
Error: Unsupported features in 5 methods
Detailed message:
Error: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.ClassLoader.defineClass(String, byte[], int, int) is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
...
Error: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.ClassLoader.defineClass(String, byte[], int, int, ProtectionDomain) is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
...
Error: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.ClassLoader.findLoadedClass(String) is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
...
Error: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.ClassLoader.findLoadedClass(String) is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
...
Error: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.ClassLoader.loadClass(String, boolean) is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
To diagnose the issue, you can add the option --report-unsupported-elements-at-runtime. The unsupported element is then reported at run time when it is accessed the first time.

Options and specify that all packages and classes are initialized during the native image generation, except for the two: org.codehaus.groovy.control.XStreamUtils and groovy.grape.GrapeIvy.

With --no-fallback option we want to force the native image compiler that we expect the native image is either generated correctly, or the compilation fails. Without this option set, the compiler falls back to the regular JDK execution in case of an error faced during the compilation. When it happens, we see the following message in the console log.

Listing 13. Fallback strategy in case of an error during image compilation
Warning: Image 'randomnumber' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation).

The --no-server option informs the compiler that we don’t want to use image-build server. We also set the same classpath we set when running Groovy as a Java program. And the last line contains the name of the RandomNumber.class file.

The compilation takes approximately 60 seconds and this is the output we should expect.

Listing 14. The expected native image compilation console output
$ native-image --allow-incomplete-classpath \
    --report-unsupported-elements-at-runtime \
    --initialize-at-build-time \
    --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \
    --no-fallback \
    --no-server \
    -cp ".:$GROOVY_HOME/lib/groovy-2.5.7.jar" \
    RandomNumber
[randomnumber:30836]    classlist:   2,543.84 ms
[randomnumber:30836]        (cap):     842.60 ms
[randomnumber:30836]        setup:   2,037.49 ms
[randomnumber:30836]   (typeflow):  10,398.18 ms
[randomnumber:30836]    (objects):  12,716.21 ms
[randomnumber:30836]   (features):     502.37 ms
[randomnumber:30836]     analysis:  24,049.68 ms
[randomnumber:30836]     (clinit):     309.26 ms
[randomnumber:30836]     universe:     952.52 ms
[randomnumber:30836]      (parse):   2,359.79 ms
[randomnumber:30836]     (inline):   3,216.99 ms
[randomnumber:30836]    (compile):  17,702.26 ms
[randomnumber:30836]      compile:  24,547.04 ms
[randomnumber:30836]        image:   2,308.60 ms
[randomnumber:30836]        write:     352.50 ms
[randomnumber:30836]      [total]:  56,941.42 ms

Running standalone native image

The compilation succeeds and we can see randomnumber executable file in the current folder.

Listing 15. The current folder with randomnumber executable file
$ ls -lah randomnumber
-rwxrwxr-x 1 wololock wololock 21M 05-11 13:13 randomnumber

Let’s run it and see the result.

Listing 16. Running executable file for the first time
$ ./randomnumber
The random number is: 397
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 org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:255)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at groovy.lang.Closure.call(Closure.java:405)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6648)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6548)
	at RandomNumber.main(RandomNumber.groovy:7)

Something is broken. The first line The random number is: 397 gets printed correctly, but it fails when trying to invoke RandomNumber$_main_closure1.doCall(int). How is that?

This method represents the closure we pass to the (0..random).sum() method. The problem is that the process of the doCall(int) method lookup uses reflection. And even though the native image supports runtime reflection[2], in some cases it is not able do determine it correctly, thus it requires additional configuration provided by the user.

Reflection configuration

The manual reflection configuration for GraalVM native image is fairly simple[3]. All we have to do is to create a JSON configuration file and add -H:ReflectionConfigurationFiles=…​ to the command line. We can either configure class that will be used reflectively using helper options like allDeclaredMethods, or we can manually provide a list of methods (and their parameters) we expect to get invoked using reflection. To keep this example simple, we will use the first approach.

Listing 17. Our exemplary reflection.json file
[
  {
    "name": "RandomNumber$_main_closure1",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  }
]

Let’s recompile the native image using reflection configuration.

Listing 18. Recompiling native image using reflection configuration
$ native-image --allow-incomplete-classpath \
    --report-unsupported-elements-at-runtime \
    --initialize-at-build-time \
    --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \
    --no-fallback \
    --no-server \
    -cp ".:$GROOVY_HOME/lib/groovy-2.5.7.jar" \
    -H:ReflectionConfigurationFiles=reflections.json \
    RandomNumber
[randomnumber:14904]    classlist:   2,465.48 ms
[randomnumber:14904]        (cap):     847.33 ms
[randomnumber:14904]        setup:   1,956.50 ms
[randomnumber:14904]   (typeflow):  10,908.61 ms
[randomnumber:14904]    (objects):  14,070.69 ms
[randomnumber:14904]   (features):     389.80 ms
[randomnumber:14904]     analysis:  26,006.96 ms
[randomnumber:14904]     (clinit):     368.34 ms
[randomnumber:14904]     universe:   1,018.86 ms
[randomnumber:14904]      (parse):   2,536.26 ms
[randomnumber:14904]     (inline):   3,122.56 ms
[randomnumber:14904]    (compile):  18,851.47 ms
[randomnumber:14904]      compile:  25,996.75 ms
[randomnumber:14904]        image:   2,547.31 ms
[randomnumber:14904]        write:     375.97 ms
[randomnumber:14904]      [total]:  60,535.87 ms

We can run the program again to see if it works.

$ ./randomnumber
The random number is: 869
java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$521
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
	at java.lang.ClassLoader.loadClass(Target_java_lang_ClassLoader.java:131)
	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:3226)
	at groovy.lang.MetaClassImpl.chooseMethod(MetaClassImpl.java:3188)
	at groovy.lang.MetaClassImpl.getNormalMethodWithCaching(MetaClassImpl.java:1399)
	at groovy.lang.MetaClassImpl.getMethodWithCaching(MetaClassImpl.java:1314)
	at groovy.lang.MetaClassImpl.getMetaMethod(MetaClassImpl.java:1229)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1082)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6655)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6548)
	at RandomNumber.main(RandomNumber.groovy:7)
Exception in thread "main" groovy.lang.GroovyRuntimeException: Failed to create DGM method proxy : java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$521
	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:3226)
	at groovy.lang.MetaClassImpl.chooseMethod(MetaClassImpl.java:3188)
	at groovy.lang.MetaClassImpl.getNormalMethodWithCaching(MetaClassImpl.java:1399)
	at groovy.lang.MetaClassImpl.getMethodWithCaching(MetaClassImpl.java:1314)
	at groovy.lang.MetaClassImpl.getMetaMethod(MetaClassImpl.java:1229)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1082)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6655)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6548)
	at RandomNumber.main(RandomNumber.groovy:7)
Caused by: java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$521
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
	at java.lang.ClassLoader.loadClass(Target_java_lang_ClassLoader.java:131)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.createProxy(GeneratedMetaMethod.java:101)
	... 12 more

Failed again. This time it couldn’t find a class org.codehaus.groovy.runtime.dgm$521. This is one of the classes that represent Groovy dynamic methods - methods that extend e.g. JDK classes with the new methods. This class is also accessed through reflection, let’s add to our reflection.json configuration file.

Listing 19. Updated reflections.json file
[
  {
    "name": "RandomNumber$_main_closure1",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  },
  {
    "name": "org.codehaus.groovy.runtime.dgm$521",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  }
]

Let’s recompile the native image using the same command as before. When the compilation is done, let’s see if it works.

$ ./randomnumber
The random number is: 853
java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$1180
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
	at java.lang.ClassLoader.loadClass(Target_java_lang_ClassLoader.java:131)
	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:3226)
	at groovy.lang.MetaClassImpl.chooseMethod(MetaClassImpl.java:3188)
	at groovy.lang.MetaClassImpl.getNormalMethodWithCaching(MetaClassImpl.java:1399)
	at groovy.lang.MetaClassImpl.getMethodWithCaching(MetaClassImpl.java:1314)
	at groovy.lang.MetaClassImpl.getMetaMethod(MetaClassImpl.java:1229)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1082)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6655)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6548)
	at RandomNumber.main(RandomNumber.groovy:7)
Exception in thread "main" groovy.lang.GroovyRuntimeException: Failed to create DGM method proxy : java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$1180
	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:3226)
	at groovy.lang.MetaClassImpl.chooseMethod(MetaClassImpl.java:3188)
	at groovy.lang.MetaClassImpl.getNormalMethodWithCaching(MetaClassImpl.java:1399)
	at groovy.lang.MetaClassImpl.getMethodWithCaching(MetaClassImpl.java:1314)
	at groovy.lang.MetaClassImpl.getMetaMethod(MetaClassImpl.java:1229)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1082)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6655)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.sum(DefaultGroovyMethods.java:6548)
	at RandomNumber.main(RandomNumber.groovy:7)
Caused by: java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.dgm$1180
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
	at java.lang.ClassLoader.loadClass(Target_java_lang_ClassLoader.java:131)
	at org.codehaus.groovy.reflection.GeneratedMetaMethod$Proxy.createProxy(GeneratedMetaMethod.java:101)
	... 12 more

Failed again. This time the class org.codehaus.groovy.runtime.dgm$1180 cannot be found. Let’s add it to the reflections.json configuration file.

Listing 20. Another update to reflections.json file
[
  {
    "name": "RandomNumber$_main_closure1",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  },
  {
    "name": "org.codehaus.groovy.runtime.dgm$521",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  },
  {
    "name": "org.codehaus.groovy.runtime.dgm$1180",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true
  }
]

After updating the configuration file, let’s recompile the image using the same command as before. When it’s done, it’s time to run the program.

Listing 21. Working native image
$ ./randomnumber
The random number is: 859
The doubled sum of numbers between 0 and 859 is 738740

It worked! You also noticed that the reaction time is much better compared to the previous attempts (running Groovy code as a Java program). Let’s measure native image execution time.

Listing 22. The native image execution time
time ./randomnumber
The random number is: 580
The doubled sum of numbers between 0 and 580 is 336980

real	0m0,008s
user	0m0,005s
sys	0m0,003s

This is really nice - 8 ms. And here is how does it look like compared to the previous results.

graalvm groovy execution time

As you can see, GraalVM’s native image outperforms the two previous attempts.

Automated reflection configuration

I guess we both agree, that this manual reflection configuration was pretty annoying. We added a class to a configuration, then we recompiled the native image just to get another exception with a different missing class. In case of a such simple program we had to add three classes to the reflection configuration. We can imagine how ineffective would it be in case of a much more complex example.

Luckily, there is a solution to this problem. GraalVM’s JDK is distributed with native-image-agent - a Java agent that can be used to run our program with GraalVM’s JDK that introspects the code usage. It can detect all reflection for us (and not only that).

Let’s give it a try. Firstly, we need to run our compiled Groovy code as a Java program with the native-image-agent enabled.

Listing 23. Running as a Java program with the agent enabled
$ java -agentlib:native-image-agent=config-output-dir=conf/ \(1)
    -cp ".:$GROOVY_HOME/lib/groovy-2.5.7.jar" RandomNumber

The program executes like it did before, but know it created 4 configuration files in the folder we specified with parameter (in this case I used conf/ folder). Here are the files that got created.

Listing 24. Automatically generated configuration files for the native image builder
$ tree conf
conf
├── jni-config.json
├── proxy-config.json
├── reflect-config.json
└── resource-config.json

0 directories, 4 files

If you open conf/reflect-config.json file you will see that it contains tons of classes configured for the reflective access. (In my case this file is 579 lines long.)

Before you continue, you need to modify conf/reflect-config.json file a bit. There is a know bug in GraalVM 19.0.0 that causes problems when the class java.lang.reflect.Executable is added to the reflection configuration. (It will be fixed in the next release, so if you use a newer version, you might skip this part.) The good thing is that you can simply remove it from conf/reflect-config.json file and you are OK. Search for:

{
  "name":"java.lang.reflect.Executable",
  "allDeclaredMethods":true,
  "methods":[
    {"name":"getAnnotation","parameterTypes":["java.lang.Class"] },
    {"name":"getDeclaredAnnotations","parameterTypes":[] },
    {"name":"getDeclaringClass","parameterTypes":[] },
    {"name":"getModifiers","parameterTypes":[] },
    {"name":"getName","parameterTypes":[] },
    {"name":"getTypeParameters","parameterTypes":[] },
    {"name":"isSynthetic","parameterTypes":[] }
  ]
},

and simply remove it.

The last thing we have to do is to remove -H:ReflectionConfigurationFiles parameters and use the -H:ConfigurationFileDirectories parameter instead. It loads not only reflection configuration files, but also remaining three configurations for proxies, JNI, and resources.

Listing 25. Using automatically generated reflection configuration file
$ native-image --allow-incomplete-classpath \
    --report-unsupported-elements-at-runtime \
    --initialize-at-build-time \
    --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \
    --no-fallback \
    --no-server \
    -cp ".:$GROOVY_HOME/lib/groovy-2.5.7.jar" \
    -H:ConfigurationFileDirectories=conf/ \
    RandomNumber

It compiles in 70 seconds.

[randomnumber:6854]    classlist:   2,495.24 ms
[randomnumber:6854]        (cap):     924.98 ms
[randomnumber:6854]        setup:   2,170.19 ms
[randomnumber:6854]   (typeflow):  12,968.96 ms
[randomnumber:6854]    (objects):  17,112.64 ms
[randomnumber:6854]   (features):     568.95 ms
[randomnumber:6854]     analysis:  31,241.39 ms
[randomnumber:6854]     (clinit):     447.44 ms
[randomnumber:6854]     universe:   1,339.22 ms
[randomnumber:6854]      (parse):   2,724.16 ms
[randomnumber:6854]     (inline):   4,752.75 ms
[randomnumber:6854]    (compile):  20,960.41 ms
[randomnumber:6854]      compile:  29,926.05 ms
[randomnumber:6854]        image:   2,818.74 ms
[randomnumber:6854]        write:     373.36 ms
[randomnumber:6854]      [total]:  70,552.29 ms

And see if it works with those automatically generated configuration files.

$ ./randomnumber
The random number is: 347
The doubled sum of numbers between 0 and 347 is 120756

Cowabunga! No problems this time!

Limitations

Even though we compiled the native image successfully, we need to be aware of a few significant limitations. Groovy is not a first class citizen for GraalVM’s ahead-of-time compilation by design, and that is why you can’t expect that your Groovy program will compile to the native image successfully. Below is the list of the major limitations that cannot be avoided.

  • GraalVM’s SubstrateVM does not support dynamic class loading, dynamic class generation, and bytecode InvokeDynamic. This limitation makes dynamic Groovy scripts and classes almost 99% incompatible with building native images. That is why we had to turn on static compilation in the example described above.

Here you can read more about SubstrateVM limitations.
  • Metaprogramming features don’t work in the native image.

  • Coercing closures to other specific types (e.g. functional interfaces used with Java 8 Stream API) does not work.

Conclusion

I hope you have learned something interesting from this blog post. If you are interested in learning more about Groovy and GraalVM, checkout my other blog posts you can find in the section below.

You can also check my wololock/graalvm-groovy-examples GitHub repository, where I collect some of the demos and examples I create during my experiments. Feel free to test it, experiment on your side and contribute to the project. GraalVM is fascinating and quite challenging piece of technology. The more we experiment with it and learn how to use it most effectively, the more we can help other people adopting it.

Useful resources

Here you can find a list of blog posts I found useful when I was working on this article.

This post was updated after GraalVM CE 19.0.0 was released on May 9th 2019. The source of the original blog post published on October 28th 2018 is still in the Github repository history and can be found here.

1. As explained by Condrut Stancu in the following Github issue comment https://github.com/oracle/graal/issues/346#issuecomment-383015796

Szymon Stepniak

Groovista, Upwork's Top Rated freelancer, Toruń Java User Group founder, open source contributor, Stack Overflow addict, bedroom guitar player. I walk through e.printStackTrace() so you don't have to.