GraalVM Native Images
Compile a Spring Boot app ahead-of-time into a native binary: millisecond startup and low memory, with the trade-offs that come with it.
On this page
A Spring Boot app on the JVM starts in a second or two and warms up as the JIT compiler optimizes hot paths. That's fine for a long-running server - but painful when you want to scale to zero and back, run serverless functions, or pack many small services densely. GraalVM native images compile your app ahead of time into a standalone binary that starts in milliseconds and uses a fraction of the memory.
Ahead-of-time, not just-in-time
Normally Java compiles source to bytecode, and the JVM's JIT compiler turns hot bytecode into machine code while the app runs. GraalVM's native-image tool instead does AOT (ahead-of-time) compilation: at build time it analyzes your whole app and produces a native executable containing only the code that's reachable - no JVM needed at runtime:
# Spring Boot's build plugin drives GraalVM native-image
./mvnw -Pnative native:compile
./target/bookvault # a native binary - starts in ~50ms, tens of MB of RAMThe results are dramatic: startup drops from ~1-2s to tens of milliseconds, and memory falls several-fold. For a service that scales up and down constantly, or a function billed per-invocation, that changes the economics.
The catch: a closed world
AOT's power comes from a closed-world assumption - at build time the tool must see all the code that could run. Java features that decide things at runtime fight this:
- Reflection, dynamic proxies, and runtime class loading must be known ahead of time (declared in hints), or the code isn't included and fails at runtime.
- Builds are slow (minutes) and memory-hungry compared with a normal compile.
- You lose the JIT's peak-throughput optimizations and the rich runtime tooling that attaches to a JVM.
The good news: Spring Boot 3+ was re-architected for exactly this. Its AOT engine and the Spring
libraries generate the necessary reachability hints for you, so a standard Spring app usually goes
native without manual configuration. When you use reflection in your own code, you supply hints via
RuntimeHintsRegistrar.
<!-- enable the GraalVM native build support -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>A full restaurant kitchen (the JVM) can cook anything on demand - flexible, but it takes time to fire up and heat every station. A pop-up stall built for one menu (a native image) is prepped ahead for exactly the dishes it will serve: it opens in seconds and needs a tiny footprint - but it can't suddenly cook something that wasn't planned. AOT trades the JVM's runtime flexibility for instant, lean startup by deciding everything in advance.
Native isn't automatically better
For a long-running service, the JVM's fast peak throughput and mature tooling often win, and its 1-2s startup is irrelevant. Native images shine when startup time and memory dominate: serverless, scale-to-zero, CLIs, and very dense deployments. Choose per workload - don't go native by default.
Two BookVault deployments: (a) a steady API server that runs 24/7 behind a load balancer, and (b) a scheduled function that wakes a few times an hour to send overdue notices, then exits. For each, decide whether a GraalVM native image is worth it, and justify it in terms of startup, memory, and throughput.
What is the main benefit of a GraalVM native image, and its central trade-off?
Key takeaways
- GraalVM native-image compiles a Spring Boot app ahead of time into a standalone binary - no JVM at runtime.
- Startup drops from ~1-2s to tens of milliseconds and memory falls several-fold - great for serverless, scale-to-zero, and dense deployments.
- The trade-off is a closed-world model: reflection/proxies/dynamic loading need build-time hints, builds are slow, and you lose JIT peak throughput.
- Spring Boot 3+ generates most reachability hints automatically, so typical apps go native without manual config.
- It isn't universally better: long-running servers often prefer the JVM's throughput and tooling. Choose per workload.