Skip to content

sombriks/sample-graalvm

Repository files navigation

Sample GraalVM project

Testing the benefits of native images

Requirements

  • sdkman
  • GraalVM (java 21)
  • Gradle 8.5+

Setup

In order to play with native images, a native-capable jvm is needed.

The easiest way to get a jvm like that is using sdkman.io:

sdk list java | grep graal

Should produce an output like this:

 GraalVM CE     |     | 21.0.1       | graalce |            | 21.0.1-graalce      
                |     | 17.0.9       | graalce |            | 17.0.9-graalce      
 GraalVM Oracle |     | 21.0.1       | graal   |            | 21.0.1-graal        
                |     | 17.0.9       | graal   |            | 17.0.9-graal  

Install the proper jdk:

sdk install java 21.0.1-graal

Another option is to use the command sdk env install inside this project folder, since it has a .sdkmanrc file.

Running on jvm vs running a native build

Once you get the proper jvm, use gradle to build the project:

./gradlew build test shadowJar

It calls the shadow jar plugin to create a single, batteries included, runnable jar file.

Run it normally:

java -jar build/libs/sample-graalvm-all.jar

The expected output is more or less like this:

17:51:23.616 [main] INFO  io.javalin.Javalin - Starting Javalin ...
17:51:23.623 [main] INFO  org.eclipse.jetty.server.Server - jetty-11.0.16; built: 2023-08-25T19:43:30.438Z; git: bedff458c4dd1a716d59e17b8cb0d2042eeab291; jvm 21.0.1+12-jvmci-23.1-b19
17:51:23.743 [main] INFO  o.e.j.s.s.DefaultSessionIdManager - Session workerName=node0
17:51:23.759 [main] INFO  o.e.j.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@18ece7f4{/,null,AVAILABLE}
17:51:23.770 [main] INFO  o.e.jetty.server.AbstractConnector - Started ServerConnector@32b260fa{HTTP/1.1, (http/1.1)}{0.0.0.0:7070}
17:51:23.803 [main] INFO  org.eclipse.jetty.server.Server - Started Server@17497425{STARTING}[11.0.16,sto=0] @1173ms
17:51:23.803 [main] INFO  io.javalin.Javalin -
       __                  ___           _____
      / /___ __   ______ _/ (_)___      / ___/
 __  / / __ `/ | / / __ `/ / / __ \    / __ \
/ /_/ / /_/ /| |/ / /_/ / / / / / /   / /_/ /
\____/\__,_/ |___/\__,_/_/_/_/ /_/    \____/

       https://javalin.io/documentation

17:51:23.805 [main] INFO  io.javalin.Javalin - Javalin started in 336ms \o/
17:51:23.809 [main] INFO  io.javalin.Javalin - Listening on http://localhost:7070/
17:51:23.827 [main] INFO  io.javalin.Javalin - You are running Javalin 6.0.0-beta.4 (released December 16, 2023).

Some highlights:

  • Jar file is ~20MB in size
  • Execution startup time, according to the log, is 1173ms (jetty) + 336ms (javalin)
  • Runs on any jvm version 21 or newer

Prepare the native build

GraalVM list some libraries required so compiler works properly

Then simply convert your app using native-image:

native-image -jar build/libs/sample-graalvm-all.jar -o build/sample-graalvm

Sample output:

Warning: The option '-H:ReflectionConfigurationResources=META-INF/native-image/io.micrometer/micrometer-core/reflect-config.json' is experimental and must be enabled via '-H:+UnlockExperimentalVMOptions' in the future.
Warning: Please re-evaluate whether any experimental option is required, and either remove or unlock it. The build output lists all active experimental options, including where they come from and possible alternatives. If you think an experimental option should be considered as stable, please file an issue.
========================================================================================================================
GraalVM Native Image: Generating 'sample-graalvm' (executable)...
========================================================================================================================
[1/8] Initializing...                                                                                    (5,9s @ 0,17GB)
Java version: 21.0.1+12, vendor version: Oracle GraalVM 21.0.1+12.1
Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: ML-inferred
C compiler: gcc (redhat, x86_64, 13.2.1)
Garbage collector: Serial GC (max heap size: 80% of RAM)
1 user-specific feature(s):
- com.oracle.svm.thirdparty.gson.GsonFeature
------------------------------------------------------------------------------------------------------------------------
1 experimental option(s) unlocked:
- '-H:ReflectionConfigurationResources' (origin(s): 'META-INF/native-image/io.micrometer/micrometer-core/native-image.properties' in 'file:///home/sombriks/git/sample-graalvm/build/libs/sample-graalvm-all.jar')
------------------------------------------------------------------------------------------------------------------------
Build resources:
- 11,57GB of memory (75,6% of 15,32GB system memory, determined at start)
  - 12 thread(s) (100,0% of 12 available processor(s), determined at start)
    Found pending operations, continuing analysis.
    [2/8] Performing analysis...  [*****]                                                                   (28,2s @ 0,76GB)
    7.560 reachable types   (80,5% of    9.395 total)
    10.931 reachable fields  (57,6% of   18.969 total)
    39.411 reachable methods (51,0% of   77.202 total)
    2.456 types,   126 fields, and 1.523 methods registered for reflection
    58 types,    55 fields, and    53 methods registered for JNI access
    4 native libraries: dl, pthread, rt, z
    [3/8] Building universe...                                                                               (4,0s @ 0,85GB)

Warning: Resource access method java.lang.ClassLoader.getResource invoked at org.eclipse.jetty.util.TypeUtil.getClassLoaderLocation(TypeUtil.java:644)
Warning: Resource access method java.lang.ClassLoader.getResources invoked at org.slf4j.LoggerFactory.findPossibleStaticLoggerBinderPathSet(LoggerFactory.java:230)
Warning: Aborting stand-alone image build due to accessing resources without configuration.
------------------------------------------------------------------------------------------------------------------------
                        4,1s (10,3% of total time) in 86 GCs | Peak RSS: 1,64GB | CPU load: 8,44
========================================================================================================================
Finished generating 'sample-graalvm' in 38,4s.
Generating fallback image...
Warning: Image 'sample-graalvm' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).

Then Execute it:

./build/sample-graalvm

Output is quite the same of jar version, but:

  • Executable file is ~10MB in size
  • Execution startup time, according to the log, is 840ms (jetty) + 287ms (javalin)
  • The binary is a regular executable (ELF 64-bit LSB executable, x86-64)

There is one important note, however. This warning in the output:

Warning: Image 'sample-graalvm' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).

It means that we still need a proper jvm installed to run it.

The binary produced by adding the suggested --no-fallback flag is almost 40MB in size and simply does not work.

If the code uses reflection there is no way a native image ges that properly, so the binary is just a fancy way to produce runtime errors.

Docker

The usual trick to create the jar file and then just wrap it inside an image is supposed to work here, just using an extra step to transform the jar into a native image.

Which means a multi-stage build.

There are official GraalVM images available on GitHub. There are no official GraalVM images on docker hub.

Another drawback was java version inside GraalVM images. It's still java 17 and because of that it was needed to downgrade java to 17 in build.gradle.kts.

Make sure you're built the shadow jar already:

./gradlew build shadowJar

Build the docker image using the sample dockerfile:

docker build -f src/infrastructure/Dockerfile -t sombriks/sample-graalvm:testing .

Test it with docker as a regular docker image:

docker run --rm -it -p 7070:7070 sombriks/sample-graalvm:testing

Since this is a fallback image, you still need the original jar and a container image able to provide a jvm.

See the dockerfile for more details.

Conclusion

All in all keep jvm dependency isn't a bad deal and the performance gain is promising.

But the docker image should work and due to the need of several architectures support, one could argue that native images are a no-go for the moment.

As future experiment let's try to use a more vanilla project, without kotlin.