Spring Boot and GraalVM Native Images: A Match Made in Heaven?
In this article, I’ll share my experience with Spring Boot and GraalVM Native Images. We'll compare 'Just In Time' and 'Ahead Of Time' compilation, explore Spring Boot's support for both, and determine if Spring Boot and GraalVM are a match made in heaven or hell. Let's dive in!
GraalVM
GraalVM is an advanced JDK capable of compiling Java applications “ahead of time” into standalone binaries. The generated binaries are smaller, start up 100 times faster, provide peak performance with no warmup, and use less memory and CPU than applications running on a traditional JVM. [Wow!]
But there's even more. GraalVM supports JavaScript, Ruby, Python, and R, thanks to the Truffle Language Implementation Framework. This library allows us to build tools and interpreters for other languages, meaning the number of supported languages could increase in the future.
JIT vs AOT
This is starting to sound too good to be true. So, what's the catch? As usual, there's a tradeoff, and it lies in the differences between 'just in time' and 'ahead of time' compilation.
Just In Time (JIT)
Just in time compilation translates code into machine language at runtime, allowing for dynamic optimizations based on the program’s behavior.
In a typical javac compilation, our Java code is translated into Java bytecode, which is not directly executable by the machine. That’s why we need a JVM to interpret and translate this bytecode into machine code at runtime.
What do we gain from this? First, portability. We compile our code once and run it on any operating system and hardware. Second, the JVM can perform adaptive optimizations at runtime, such as inlining, dead code elimination, loop unrolling, and much more.
Ahead Of Time (AOT)
Ahead of time compilation translates code directly into machine language, producing a standalone binary. As a result, the generated executable starts up faster and requires less memory and CPU because there is no interpretation and translation at runtime.
However, this approach introduces many restrictions and limitations and makes compilation slower and resource intensive. Let’s see why.
First, our executable is not portable. If we want to run our app on two different operating systems, we need to compile it twice, as we are generating machine-specific code at build time.
Second, we lose the opportunity for runtime optimizations since our app is executing as a process on the operating system which is not mediated by a JVM.
Finally, and most importantly, the compilation is performed under a “Closed World Assumption”. This means that all the bytecode that can be called at runtime must be known at build time which poses significant challenges for Spring Boot.
Closed World Assumption
Under a closed-world assumption:
- Unreachable code at build time is removed from the executable
- GraalVM must be informed about dynamic elements like reflection, resources, serialization and dynamic proxies
- The application class path is fixed at build time and cannot change
- There’s no lazy class loading; everything is included in the executable and loaded into memory at startup
Why is this problematic for Spring Boot?
Spring Boot challenges in a Closed World Assumption
A core feature of Spring Boot is its ability to auto-configure applications at runtime by inspecting their state and environment. It relies heavily on reflection, resources, serialization and dynamic proxies; which are all elements GraalVM doesn’t fully support out of the box. However, under a closed-world assumption beans cannot change at runtime!
To enable native image compilation, Spring Boot must statically analyze all code paths during the build and generate configuration files and hints so the GraalVM can correctly create the executable. In some cases, it’s difficult or impossible to fully support dynamic behavior, which is why profiles have limitations and properties cannot change based on conditions.
These limitations extend to additional libraries and frameworks used in our applications, so it's crucial to verify their compatibility with GraalVM. GraalVM has a page listing some of the supported libraries. However, be aware that even listed libraries might support GraalVM with limitations, which might make them unusable for your specific use case.
Native Image / AOT Use cases
As I mentioned, there’s always a trade-off. Native image compilation generates applications that start up fast and consume less CPU and memory, but comes at the expense of portability, increased build time, and reduced compatibility with third-party libraries.
So, when is it best to use Spring Boot and GraalVM together?
The answer is simple: never. I'll go a step further and say that if you develop in Java, you should not use ahead-of-time compilation at all.
This is obviously my personal opinion, and I acknowledge that there are scenarios where AOT is useful in Java. However, they are the exception and not relevant to most developers.
If we look at documentation and blogs, native executables are recommended for deploying applications in containerized environments, particularly when combined with 'functions as a service' platforms, or when creating lightweight executables like CLIs.
In all other scenarios, a JVM is likely better or on par, as highlighted by Dan Vega, a Spring Developer Advocate. For a high-traffic website, we don’t want our microservices to start up and shut down for every single request. Our applications are long-running processes, and the effort needed to start them up quickly is offset by the JVM's ability to optimize dynamically at runtime. With redundant deployments, we can achieve high availability, so how fast our application starts doesn’t matter that much, as long as it starts in a reasonable amount of time.
This means that the only clear selling point for native executables is short executions—scenarios where we want to launch the process, do something quick, and exit. In these cases, I wouldn’t use Spring or Java. There are languages better suited for this, like Go, JavaScript, or Python. Since we are talking about small applications, it’s worth the effort of learning a new language and toolchain to avoid the complications and limitations introduced by GraalVM Native Image.
I want to highlight that the impact on CI/CD pipelines could be massive. When I say that compilation takes longer and is more resource-intensive, I mean it. A just-in-time build that lasts few seconds could take several minutes if performed ahead of time.
Conclusion
In conclusion, if you are developing a web application in Spring Boot, don’t even bother with GraalVM Native Images. If you’re developing a CLI or something in the context of serverless or function as a service, don’t use Java.
If you disagree, leave me a comment!
If you agree, like and share so that everyone can learn something new!