Native Spring Boot Applications with GraalVM (Part 1) - Introduction

Cem Berke Çebi
Trendyol Tech
Published in
7 min readAug 27, 2021

Hello everyone, through this article series we will examine the Spring Native with GraalVM. There will be two different articles in this series. In our first article, we will look into Spring Native, GraalVM, ahead of time (AOT), just-in-time (JIT) theoretically. In our second article, we will share our experiences about how we implement Spring native in Trendyol with performance metrics and the implementation process.

Spring Native ensures that Spring programs can be compiled to a native executable. In order to obtain these native executables, Spring Native compiles native images using the GraalVM Native Image compiler.

What is GraalVM?

GraalVM is a Java development environment that enables developers to write and execute Java code. It is a Java Virtual Machine (JVM) and Java Development Kit (JDK) developed by Oracle. It is a high-performance runtime that enhances the performance and efficiency of applications.

Objectives of GraalVM are developing a faster and maintainable compiler, optimizing the performance of languages that run on the JVM, decreasing application startup times, integrating multi-language support into the Java ecosystem, and providing a set of programming tools to accomplish these goals.

GraalVM augments the JDK with an optimizing compiler that optimizes performance for individual languages and ensures interoperability for polyglot applications. Apart from Java, GraalVM also supports the following programming languages: Kotlin, JavaScript, Node.js, and Python. It enables developers to efficiently run code written in multiple languages and libraries within a single application.

Before deep-diving into the spring native, it’s better to briefly take a look at how a java application works and then what AOT and JIT compilers are because these topics are an important factor in our understanding of the Spring Native concept.

How does java work?

In order to understand the AOT and JIT, first, we need to understand how a java code works.

Let’s think that we have some sort of Java code, you will first compile it to byte code using the javac and this byte code is an intermediate representation of your code which provides portability where you can run the same bytecode across different kinds of architectures. You can use it on Intel, which is an x86 architecture, you can run it on arm and you have some other architectures like power 9, SPARC, etc. When you run this bytecode your application runs on the JVM and the JVM will convert your bytecode into the machine code and this step is also called compilation.

the basic flow of how java works

Bytecode to Machine Code (Compilation)

When your application starts it has access to the bytecode it will go one line at a time and it will use some sort of dictionary, which decides what type of conversion is needed for the bytecode to work on a running system. Inside the JVM there is a component called an interpreter, it will go through the bytecode one line at a time using a dictionary it will convert the bytecode into the machine's instructions, then it will send those instructions for the CPU to execute.

the flow of bytecode to machine code

This is the basic conversion of bytecode into machine code. However there is a problem with this flow, interpreters wasting time in compiling your code when converting each line of your bytecode into the machine code even for the repeated codes like for loop or the methods that your application uses constantly. Therefore it is very inefficient to use this process without any optimization. At this point, the C1 and C2 compilers are joining the process.

C1 compiler

JVM keeps a counter of which methods or which code snippets are executed how many times. For example, if there is a method called sumTwoNumbers, and whenever this function is called, the counter will be incremented for this specific function. Once that counter reaches a particular threshold JVM realizes that this method is going to be used a lot more times since it has already reached the threshold. Then the C1 compiler will take the function, which exceeds the threshold value, compile it then save it into an area of JVM called code cache. So the next time when this method is called instead of compiling again with an interpreter, it will pick up the compiled code from the code cache and give those instructions to the CPU to run. With the help of this process now the complication process is optimized.

the flow of bytecode to machine code with c1 compiler

C2 compiler

In the background, JVM will start collecting the runtime statistics of how your code is being executed and it will try to create control flow graphs or core parts of your code to find the most used parts of your code. Once JVM has enough statistics it will ask the C2 compiler to perform heavier optimizations and just like the C1 compiler compiled code will be saved in code cache if the same method/code found the code cache it will replace the C1’s methods with C2.

the flow of bytecode to machine code with c1 and c2 compiler

AOT and JIT

C1 and C2 compilers do not stop the application from running so based on the number of courses it will decide how many background threads to create for both C1 and C2 and while the code is being interpreted behind the scenes the compilation can go on. Once a compiled code is ready, the JVM will execute the code from the core cache instead of asking the interpreter, so all these compilations and interpretations are being done at the runtime while your application has started and is running and that is why the name just-in-time compilation.

However, JVM can do it while converting java code into bytecode and that is called ahead of time compilation, from Java 9 we have the option of converting some part of your code or libraries into the compiled code before you run the application.

Spring Native

Spring native gives its users the chance to build a native image of an application that is available to developers for free. In a single package, these native images include all of the distinct functionalities of your code, libraries, resources, and JDK, and they have been optimized to execute on a particular platform. The benefits of this process are less memory and CPU usage, and a decrease in the start-up time of the application. As a consequence of reduced system overhead and fewer garbage collection cycles, an application that starts more quickly consumes less memory and CPU.

AOT compiler support has also been introduced to Spring Native as 0.9 version, which is now available. Using Spring Native, now we can take advantage of using AOT compilation in our systems, which we discussed in the previous section of this post.

Creating a native image is not a new system, it is a concept that has been used for a long time, especially by large enterprise companies due to its benefits in terms of scalability, efficiency, and fast application launch time by optimizing at cloud and container levels.

The GraalVM virtual machine is particularly popular among Java developers who want to use native ideas in their applications. However, they encounter problems especially when they want to use Spring and GraalVM together. The most serious issue is Dependency Injection in Spring programs; it is quite difficult to convert a Spring boot application to a native image using GraalVM, particularly owing to the connections between various spring components. Since GraalVM is mostly dependent on static analysis, it has difficulties with understanding spring’s dependency injection and auto wiring models.

This is where Spring Native comes into play; Spring Native aims to solve this issue by establishing a system for easily and efficiently producing native images compatible with GraalVM.

Supported Technologies

Due to the fact that Spring Native is still a new in-development concept. It does not support all of the technologies we use in Spring. However, the number of supported technologies increases with each new version, including the following:

  • spring-boot-starter-data-jdbc
  • spring-boot-starter-data-jpa
  • spring-boot-starter-actuator
  • spring-boot-starter-data-elasticsearch
  • spring-boot-starter-data-mongodb
  • spring-boot-starter-data-redis
  • spring-boot-starter-security
  • spring-boot-starter-test

Spring Native supports many technologies other than what we wrote above. If you want to look at all supported technologies, you can use this link.

Our article ends here. In general, I tried to give theoretical information about Spring Native and GraalVM issues. After this point, you can read the second part of our article series. In the second part, detailed information about how we can compile a spring native application and its performance metrics is waiting for you.

--

--