Native Spring Boot Applications with GraalVM (Part 2)-Build Native Image & Performance Results

Burakcan Ekici
Trendyol Tech
Published in
7 min readAug 27, 2021

In the previous article, we looked deeply at Native Image from different perspectives such as GraalVM, the difference between ahead of time (AOT) and just-in-time (JIT), how it works, and pros/cons. Now, we will look native image more practically. Therefore, before starting this article, it is good to read the following article for understanding Native Image.

The project that we will evaluate in this article is accessible here. You can clone the project to your local and follow the instructions that we mentioned in the README file. You can skip the next two steps and continue from Compiling Methods if you prefer to clone the application.

Firstly, we start with opening a sample java application and then build it into a native executable. There are 2 different ways of compiling native images that are;

  • local native image build
  • docker image build

Before evaluating the above, we are producing 2 different profiles, which will be used when we compile the project, in the pom.xml file. We are adding the following code to the pom.xml file.

Now, we are ready to compile our project in both ways. We are starting with local native image build and then we will continue with docker image build.

COMPILING METHODS

Local Native Image Build

For local native image build, firstly, we need to download the relevant SDK which is 21.1.0.r11-grl. At this point, we will use SDKMAN that provides managing parallel versions of multiple SDKs. It provides a convenient CLI and API for installing, switching, removing, and listing all SDKs. After downloading the SDKMAN we will check all available SDKs with the following command;

sdk java list
available java SDKs

Among all these SDKs, the GraalVM SDKs have been marked in the red box and we will use the second one. We are installing it with the following command;

sdk install java 21.1.0.r11-grl
install relevant SDK

Then, we declare that we will use this SDK to compile the java application by executing the following command;

sdk use java 21.1.0.r11-grl
use relevant SDK

We didn’t make it default, it will just be used in the shell that we currently used. We should repeat this step whenever we open a new shell or made it default SDK.

If we list all SDKs again, we will see the mark that shows which SDK will be used. It marks the SDK we want.

check the SDK

Native Image is a technology to compile an application ahead of time into a native executable. Therefore, we need to add Native Image executable into GraalVM before compiling the project. GraalVM Updater can be used to add the Native Image executable. According to the official document, GraalVM Updater, gu , is a command-line utility to install and manage optional GraalVM language runtimes and utilities (Each of these language runtimes and utilities must be downloaded and installed separately). After this additional step, the “native-image” executable will become available in the GRAALVM_HOME/bin directory.

gu install native-image
install native-image

Now, we are ready and execute the following command for compiling the project with skip the tests.

./mvnw package -Pnative-image -DskipTests
mvn package

After the build finished successfully, under the target folder, the executable file and jar files were generated. We can run the executable file marked in the red box below by the following command;

./target/spring-native-demo
target file

Docker Image Build

Aside from the local native image build, the docker image build is another way to build projects in Native Image and generate an executable file. We execute the following command to build a docker image;

./mvnw spring-boot:build-image -Pnative-docker -DskipTests
docker build

Docker image build takes longer than local native image build. Also, it needs more resources (i.e., memory, CPU, etc.) than local native image build. But, we will see the benefit of docker image build when we run the docker image with the following command. Thanks to optimization that the docker makes during the build, it needs less startup time (less than a second) than local native image build.

docker run <image-name> => spring-native-demo:0.0.1-SNAPSHOT
docker run

TEST RESULTS

The figure below shows what we should expect when we build in ahead of time. Since, in Native Image, our applications are optimized ahead of time during compilation, the need for startup optimization is greatly reduced during startup compared to the JIT compiler.

taken from

We will evaluate all these differences mentioned deeply via compiling this application on both JVM and GraalVM, and look at the difference in their memory consumption, build time, startup time, peak throughput, and package size. In the light of the information above, we assume that, in AOT, apps are

  • faster startup speed (almost instantly)
  • lower CPU and memory consumption

than JIT, but the disadvantages brought with AOT are;

  • long build time
  • decrease on peak throughput

The test results below showed that significant differences happened between JVM and GraalVM.

Memory Consumption

Reduced memory footprint at runtime is one of the important advantages that come with Native Image together. In Native Image, all the code of the application has been already compiled before the application runs so, at runtime, the memory is managed effectively and the garbage collector works whenever it is needed. Therefore, we pretend that GraalVM has lower memory consumption than JVM.

To evaluate CPU and memory consumption, we used Grafana and compared the results.

Firstly, we compiled the project on JVM and it reached 1.6 cores in CPU and 0.56 GB memory.

Then, we switched the way to GraalVM and the difference we expected started to be seen.

After a while, our consumption downed to 1.3 cores in CPU and 0.32 GB memory.

Build Time

When we look at the build times, in AOT, all class-initialization code gets executed at build time. Therefore JVM has better build time than GraalVM.

Firstly, we built the application with the JVM profile and it was built in 1.17 seconds.

Then, we built the application with the native-image profile and it was built in 4 minutes.

Startup Time

Since all class initialization gets executed at build time, all classes have been already loaded as runtime. Also, native applications are partially initiated and it helps to prepare some part of the heap via initializing some classes at build time. Therefore, we pretend that the application should have a faster startup time in Native Image.

Firstly, we started the application that we build with the JVM profile and it started in 3.6 seconds.

Then, we started the application that we build with the native-image profile and it started in 1 second.

Peak Throughput

AOT compilers have limited resources during the build stage. Therefore, JIT compilers use better and more progressive optimization techniques than AOT compilers and it is more beneficial on peak throughput view. The result was taken from Locust shown below. JVM reached 2 times of Native Image on RPM count.

RPM comparison

Package Size

At package size view, we can consider the below graph as two parts, the two left bars show the local build results, the bars remaining show that the docker build results.

In the local build part, in AOT, the JVM utilities essential to execute itself are also packaged so it has a larger package size than JIT.

In the docker build part, in AOT, thepacketo presents some external optimizations that make package size smaller than JIT. It should be also considered Native Image does not contain JVM files, therefore the package size becomes smaller.

package size comparison

As a result, in this article, we have looked deeply at the ways to build native image and the pros/cons brought with Native Image. As you see, there is no certain point to assume that one of them is better than the other (either AOT or JIT). We aimed it helps to determine which one you could use in various cases that you are faced by understanding pros/cons.

--

--