Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

GraalVM compiles Java applications into machine executables using its Ahead-Of-Time (AOT) compiler. These executables execute directly into the target machine without using a Just-In-Time (JIT) compiler. The GraalVM-produced binaries are smaller, have a fast start-up time, and provide peak performance without any warm-up. Besides, these executables have a lower memory footprint and CPU than the applications running on JVM.

Docker lets us package software components into a Docker Image and run as a Docker container. A Docker container contains everything the application needs to run including the application code, runtime, system tools, and libraries.

In this tutorial, we’ll discuss creating a GraalVM native image of a Java application. We’ll then talk about how to use this native image as a Docker image and run it as a Docker container.

2. What Is a Native Image?

Native Image is a technology that compiles Java code ahead of time into a native executable. This native executable includes only the code required to be executed at runtime. This includes application classes, standard library classes, the language runtime, and statically linked native code from JDK.

The Native Image Builder (native-image) scans the application classes and other metadata to create a binary file specific to an operating system and architecture. The native-image tool performs a static application code analysis to determine the classes and methods reachable while the application runs. It then compiles the required classes, methods, and resources into a binary executable.

3. Benefits of Native Image

There are several benefits of a native image executable:

  • As a native image builder compiles only the resources that are needed at runtime, the size of the executable is small
  • Native executables have extremely fast start-up times as these are directly executed in the target machine without a JIT compiler
  • Provides a lesser attack surface as it packages only the required application resources
  • Useful to package in a lightweight container image such as Docker Image for fast and efficient deployment

4. Building a GraalVM Native Image

In this section, we’ll build a GraalVM native image for a Spring Boot application. First, we need to install GraalVM and set the JAVA_HOME environment variable. Second, create a Spring Boot application with the Spring Web and GraalVM Native Support dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.1.4</version>
</dependency>

We also need to add the following plugin for the GraalVM native support:

<build>
    <plugins>
        <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <version>0.9.27</version>
        </plugin>
    </plugins>
</build>

This application contains a sample rest controller:

@RestController
class HelloController {
	
    @GetMapping
    public String hello() {
	return "Hello GraalVM";
    }
}

Let’s build the native executable using the Maven command:

$mvn -Pnative native:compile

The native-maven-plugin builds the GraalVM native image. Since the GraalVM native image compiler performs static code analysis, the build time is high compared to the regular Java application compilation.

The following is the output of the GraalVM compilation:

========================================================================================================================
GraalVM Native Image: Generating 'springboot-graalvm-docker' (executable)...
========================================================================================================================
<strong>[1/8] Initializing... (42.7s @ 0.15GB)</strong>
Java version: 17.0.8+9-LTS, vendor version: Oracle GraalVM 17.0.8+9.1
Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: ML-inferred
C compiler: gcc (linux, x86_64, 11.3.0)
Garbage collector: Serial GC (max heap size: 80% of RAM)

// Omitted for clarity

<strong>[2/8] Performing analysis... [******] (234.6s @ 1.39GB)</strong>
15,543 (90.25%) of 17,222 types reachable
25,854 (67.59%) of 38,251 fields reachable
84,701 (65.21%) of 129,883 methods reachable
4,906 types, 258 fields, and 4,984 methods registered for reflection
64 types, 70 fields, and 55 methods registered for JNI access
4 native libraries: dl, pthread, rt, z
[3/8] Building universe... (14.7s @ 2.03GB)
[4/8] Parsing methods... [*******] (55.6s @ 2.05GB)
[5/8] Inlining methods... [***] (4.9s @ 2.01GB)
[6/8] Compiling methods... [**********
[6/8] Compiling methods... [*******************] (385.2s @ 3.02GB)
[7/8] Layouting methods... [****] (14.0s @ 2.00GB)
[8/8] Creating image... [*****] (30.7s @ 2.72GB)
48.81MB (58.93%) for code area: 48,318 compilation units
30.92MB (37.33%) for image heap: 398,288 objects and 175 resources
3.10MB ( 3.75%) for other data
82.83MB in total

// Omitted for clarity

Finished generating 'springboot-graalvm-docker' in 13m 7s.

// Omitted for clarity

In the above compilation output, the following are a few key points:

  • The compilation uses the GraalVM Java compiler to compile the application
  • The compiler does a reachability check for types, fields, and methods
  • Next, it builds the native executed and shows the executable size and the time taken for compilation

Post the successful build, we can find the native executable available in the target directory. This executable can be executed in the command line.

5. Building a Docker Image

In this section, we’ll develop a Docker image for the native executable generated in the previous step.

Let us create the following Dockerfile:

FROM ubuntu:jammy
COPY target/springboot-graalvm-docker /springboot-graalvm-docker
CMD ["/springboot-graalvm-docker"]

Next, let us build the Docker image using the following command:

$docker build -t springboot-graalvm-docker .

Post successful build, we can notice that springboot-graalvm-docker Docker image is available:

$docker images | grep springboot-graalvm-docker

We can execute this image using the following command:

$docker run -p 8080:8080 springboot-graalvm-docker

The above command starts the container and we can notice the Spring Boot startup logs:

// Ommited for clarity
***  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization <strong>completed in 14 ms</strong>
***  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
***  INFO 1 --- [           main] c.b.g.GraalvmDockerImageApplication      : Started GraalvmDockerImageApplication in 0.043 seconds (process running for 0.046)

The application starts in 43 milliseconds. We can access the REST endpoint by accessing the following command:

$curl localhost:8080

It shows the following output:

Hello GraalVM

6. Conclusion

In this article, we build a Docker image for a GraalVM native executable.

We started discussing the GraalVM native image and its advantages. It is useful for use cases requiring a first start-up and low memory footprint. Next, we generated the native executable of a Spring Boot application using the GraalVM native image compiler. Lastly, we developed a Docker Image with the native executable and started a Docker container with the image.

The source code for this application is available on over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!