Start Learning
Javaneer
Back to stage
Module 8·Production & Modern Spring

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.

14 min readAdvanced
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 RAM

The 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 pop-up stall vs. a full restaurant kitchen

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.

When is native worth the trade-offs?

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.
Was this lesson helpful?
Edit this page on GitHub