Spring 6 comes with a new feature that promises to optimize the performance of applications: Ahead-of-Time (AOT) compilation support.
In this article, we will explore how Spring 6’s AOT optimization feature works, its benefits, and how to use it.
2. Ahead of Time Compilation
2.1. Just-In-Time Compiler (JIT)
For the most used Java Virtual Machine (JVM), like Oracle’s HotSpot JVM and OpenJDK, when we compile the source code (.java file), the produced bytecode is stored in .class files. This way, the JVM uses a JIT compiler to convert bytecode into machine code.
Additionally, JIT compilation involves the interpretation of the bytecode by the JVM and the dynamic compilation of frequently executed code into native machine code during runtime.
2.2. Ahead of Time Compiler (AOT)
Ahead-of-Time (AOT) compilation is a technique that pre-compiles bytecode into native machine code before the application runs.
Java Virtual Machines do not commonly support this feature. However, Oracle has released an experimental AOT feature for the HotSpot JVM in the OpenJDK project called “GraalVM Native Image” that allows for the ahead-of-time compilation.
After pre-compiling the code, the computer’s processor can execute it directly, eliminating the need for the JVM to interpret the bytecode and improving the start-up time.
In this article, we won’t look at AOT Compiler in detail. Please refer to our other article for an overview of Ahead Of Time Compilation (AOT)
3. AOT in Spring 6
3.1. AOT Optimization
When we build a Spring 6 application, there are three different runtime options to consider:
- The traditional Spring application running on the JRE.
- Code generated during the AOT phase of compilation and running on the JRE.
- Code generated during the AOT phase of compilation and running in a GraalVM native image.
Let’s consider the second option, which is the brand-new feature of Spring 6 (the first is the traditional build, and the latter is the native image).
Building an application via AOT compilation has multiple advantages in terms of performance and resource consumption:
- Dead Code Elimination: The AOT compiler can eliminate code never executed at runtime. This can improve performance by reducing the amount of code that needs to be executed.
- Inlining: Inlining is a technique in which the AOT compiler replaces a function call with the actual code of the function. This can improve performance by reducing the overhead of function calls.
- Constant Propagation: AOT compiler optimizes the performance by replacing variables with their constant values that it can determine at compile-time. This eliminates the need for runtime calculations and improves performance.
- Inter-procedural optimization: The AOT compiler can optimize code across multiple functions by analyzing the call graph of the program. This can improve performance by reducing the overhead of function calls and by identifying common sub-expressions.
- Bean Definition: The AOT compiler in Spring 6 improves application efficiency by cutting out unnecessary BeanDefinition instances.
So let’s build the application with AOT optimization with the command:
mvn clean compile spring-boot:process-aot package
And then run the application with the command:
java -Dspring.aot.enabled=true -jar <jar-name>
We can set up the build plugin to enable AOT compilation by default:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <id>process-aot</id> <goals> <goal>process-aot</goal> </goals> </execution> </executions> </plugin>
3.2. Issues in AOT Optimization
When we decide to build our application with the AOT compilation, we may incur some problems, such as:
- Reflection: it allows code to call methods and access fields unknown at compile-time dynamically. The AOT compiler cannot determine the classes and methods dynamically invoked.
- Property files: the contents of a properties file can change at runtime. The AOT compiler cannot determine the property file dynamically used.
- Proxies: proxies control access to another object by providing a surrogate or placeholder for it. Because proxies can be used to redirect method calls to other objects dynamically, it can make it difficult for the AOT compiler to determine which classes and methods will be called at runtime.
- Serialization: Serialization converts an object’s state to a byte stream and vice versa. Overall it can make it difficult for the AOT compiler to determine which classes and methods will be called at runtime.
In order to identify which classes are causing issues in a Spring application, we can run an agent that provides information on reflective operations.
So let’s configure the Maven plugin to include a JVM argument to assist with this.
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments> -agentlib:native-image-agent=config-output-dir=target/native-image </jvmArguments> </configuration> <!- ... --> </plugin>
And let’s run it with the command:
./mvnw -DskipTests clean package spring-boot:run
target/native-image/ we will find generated files like reflect-config.json, resource-config.json, etc.
If something is defined inside this file, it is time to define RuntimeHints to allow the correct compilation of the executable.
In this article, we have introduced the new AOT optimization feature of Spring 6.
As always, the complete source code of the examples can be found on GitHub.