Running Spring Boot apps as GraalVM native images

Photo by Alexander Sinn

Indrek Ots
by Indrek Ots
5 min read

Categories

  • articles

Tags

  • java
  • graalvm
  • native image
  • containers

Notice! This post is more than a year old. It may be outdated.

GraalVM is a high-performance polyglot virtual machine for running applications written in JavaScript, JVM-based languages, LLVM-based languages and others. For Java workloads it’s used as a regular JVM but with performance advantages. An interesting feature that GraalVM brings to the table is its ability to create ahead-of-time (AOT) compiled native images of JVM applications which promise faster startup times and lower memory footprint. In this post we’re going to focus on how to create native binaries from Spring applications.

GraalVM Native Image 101

Java applications are compiled into bytecode using javac. During application runtime, the JVM loads class files into memory and analyzes the program’s performance for hot spots; hence the name “HotSpot JVM”. The just-in-time (JIT) compiler compiles parts of the application which are executed repeatedly into native machine code. JIT compilation, however, requires processor time and memory which affects the startup time of the application.

GraalVM native image allows us to ahead-of-time compile our JVM application into machine code. It statically analyzes application’s bytecode, finds all classes and methods that are ever reachable and compiles them into a native executable. The output is a platform specific executable binary.

For instance, let’s build a native image from the following “Hello World” program.

class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello World");
  }
}

First, we need to compile the Java code with javac

$ javac HelloWorld.java

and then use native-image to build an executable binary from the class file.

$ native-image HelloWorld

To start the application, simply

$ ./helloworld
Hello World

Native Image Java Limitations

GraalVM native image static analysis requires a closed-world assumption. It needs to know all the bytecode that’s ever reachable ahead of time during image generation. Thus, not all Java features are supported or they require configuration.

For example, dynamic class loading/unloading is not supported. Reflection requires configuration. CGLIB proxies don’t work with native images. On the other hand, JDK proxies are supported but require configuration. Additionally, you need to tell native image about all resource accesses.

Configuration is supplied in a form of a JSON document. For instance, to configure reflection, you create the following file and use the -H:ReflectionConfigurationFiles command line flag to specify the file location to the native-image command.

[
  { "name":"java.lang.Object" },
  { "name":"org.apache.naming.factory.ResourceFactory", "methods" : [{"name": "<init>","parameterTypes":[]}] },
  ...
]

Similar files have to be created to configure dynamic proxies, JNI and resource accesses. Doing all this by hand is a lot of work though, especially when we’re dealing with a large application. Fortunately, there’s a Java agent that can generate the configuration. It observes the behavior of an application running in a JVM and produces configuration files needed for native image generation.

To get a complete set of configuration files, you would need to exercise all code paths in your application. A tests suite with 100% coverage would do the trick but in reality, test suites never test all paths. Therefore, manual modification of these configuration files might be needed as well.

Spring and GraalVM Native Image

Starting from Spring Framework 5.1, initial support for GraalVM native images was provided. The 5.2 development cycle is focusing on improving the integration and full support without requiring extra configuration or workarounds is one of the themes of the upcoming Spring Framework 5.3 release.

Example Spring Boot Application

The spring-graal-native Github repository has examples of how to build a native image from a Spring Boot application. The project implements a Graal Feature which does the heavy lifting when it comes to configuring reflection, proxies etc.

Features allow clients to intercept the native image generation and run custom initialization code at various stages. All code within feature classes is executed during native image generation, and never at run time.

Let’s focus on a hello-world-level Spring Boot example—Spring MVC with Tomcat. Keep in mind, as of writing this, the example expects you’re using GraalVM 19.2.1 and you have the native-image plugin installed.

Before building the example, we need to compile the Spring Graal Feature. The root of the repository has a bash script to do that.

$ ./build-feature.sh

Once that’s finished, let’s move to the Spring MVC example folder and execute compile.sh. It builds the Spring app using Maven and then generates a GraalVM native image. The native-image command is supplied with the location of the Spring Graal Feature and various configuration files. Be warned that native image generation takes considerably longer than a regular Maven build. Also, the process likes to use a lot of RAM. When finished, navigate to the target folder and start the app.

$ ./springmvc-tomcat
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::

...

INFO: Started TomcatApplication in 0.054 seconds (JVM running for 0.057)

Notice the fast startup time of 0.054 seconds. For comparison, when running the app in a JVM, the reported startup time for me was 1.455 seconds.

Summary

GraalVM native image enables us to build ahead-of-time compiled JVM applications that start very fast and use less memory. That’s definitely useful for short lived processes, especially in the serverless scene where you’re billed by the millisecond.

Due to classpath scanning and auto-configuration, Spring Boot apps are very CPU hungry during startup. When starting multiple Spring Boot apps simultaneously on a shared host, they start to compete for CPU and the startup time increases. Orchestration tools could even kill the processes because they didn’t start fast enough. Fast-starting ahead-of-time compiled Spring Boot apps could be the answer to the problem.

Containerized Spring Boot applications have something to gain as well. Since a native binary has everything it needs, there’s no need to bake a JRE into the container anymore. We can build smaller Docker images.

Several microservice-focused frameworks have already made use of the native image feature (e.g. Quarkus, Micronaut, Helidon). Although, Spring Boot does not yet fully support native image generation, I think it will be a significant addition to the framework.