GraalVM & native images in Java
Ahead-of-time compilation, fast startup, reflection limitations.
Compiled ahead of time
GraalVM's native-image compiles your app ahead of time into a standalone executable. It contains your code, the JDK classes you need and a small runtime (Substrate VM, with its own GC). No separate JVM is needed, startup takes milliseconds, and memory use is smaller.
native-image -jar app.jar app
./app # starts in millisecondsThe closed world
The build relies on a closed-world assumption: everything reachable must be known at build time. A static analysis starts from main and includes only code it can prove is used. Everything else is left out.
Works on the JVM, not native
A native image throws ClassNotFoundException for Class.forName(config.get("handler")), though it works on the JVM. Why?
The class was never seen by the build-time analysisNative images can't call Class.forName at allThe class must be final
Show the answer
The class name is only known at runtime, so static analysis couldn't see it and left it out. Register it in reachability metadata.
Reachability metadata
Dynamic features (reflection, dynamic proxies, resources) need metadata telling the build what to keep. The tracing agent records what your app uses while it runs on a normal JVM. Spring Boot AOT, Quarkus and Micronaut generate most of it for you.
"Native is always faster"
Native images win at startup and footprint. But builds are long and memory-hungry, and without runtime profiling, peak throughput can trail the HotSpot JIT, which optimizes from real behavior (profile-guided optimization narrows the gap). Highly dynamic apps fit poorly.
Who benefits most?
A 6-hour batch job, a desktop IDE that generates bytecode at runtime, or a serverless function started per burst of requests. Which gains most from a native image?
Think about it, then reveal the answer
The serverless function. Short-lived, frequently started processes gain most from instant startup. Long-running jobs want peak JIT throughput, and highly dynamic apps clash with the closed world.
Where native images shine
CLIs, serverless functions and containers that scale to zero. And if a closed world is too strict for you, HotSpot is catching up: since JDK 24, AOT caches from Project Leyden cut startup on a regular JVM too.
Key takeaways
- AOT-compiled: no JVM or JIT warm-up at runtime
- Closed world: dynamic features need metadata
- Spring Boot AOT, Quarkus and Micronaut generate most of it
- On HotSpot, JDK 24+ AOT caches (Project Leyden) also cut startup
💡 A JIT-run app is a chef cooking to order; a native image is a meal prepped in advance: served instantly, but the menu is fixed.
The Graal compiler at the heart of native-image is itself written in Java: a compiler for Java bytecode, written in Java.
Practice questions
A native image throws ClassNotFoundException for `Class.forName(config.get("handler"))`, though it works on the JVM. Why?
- The class wasn't seen by the closed-world analysis; register it in reachability metadata
- The config file is read too late
- The class must be final
- Native images don't support Class.forName at all
Check your answer
The class wasn't seen by the closed-world analysis; register it in reachability metadata. Static analysis only includes classes it can prove are used. Classes reached through runtime strings must be registered, for example via the tracing agent.
Which workload benefits most from a native image?
- A desktop IDE with heavy runtime bytecode generation
- A batch job that runs for six hours and needs maximum throughput
- A serverless function started per burst of requests that must answer in milliseconds
- An app that loads plugins from unknown JARs at runtime
Check your answer
A serverless function started per burst of requests that must answer in milliseconds. Short-lived, frequently started processes gain most from instant startup. Long-running and highly dynamic apps fit the JIT better.