🛠️ Testing, Tools & Ecosystem · Advanced

GraalVM & native images in Java

Ahead-of-time compilation, fast startup, reflection limitations.

🧩 The mysteryYour Java function starts in 20 milliseconds instead of 2 seconds, with no JVM installed on the machine. Is it still Java?

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 milliseconds

The 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.

🔮 Predict it

Works on the JVM, not native

A native image throws ClassNotFoundException for Class.forName(config.get("handler")), though it works on the JVM. Why?

  1. The class was never seen by the build-time analysis
  2. Native images can't call Class.forName at all
  3. The 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.

⚠️ The trap

"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.

🤔 Think first

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.

💼 In the real 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

  1. AOT-compiled: no JVM or JIT warm-up at runtime
  2. Closed world: dynamic features need metadata
  3. Spring Boot AOT, Quarkus and Micronaut generate most of it
  4. 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.

🤯 Did you know?

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?

  1. The class wasn't seen by the closed-world analysis; register it in reachability metadata
  2. The config file is read too late
  3. The class must be final
  4. 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?

  1. A desktop IDE with heavy runtime bytecode generation
  2. A batch job that runs for six hours and needs maximum throughput
  3. A serverless function started per burst of requests that must answer in milliseconds
  4. 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.

That's the last world on the map. You've gone from println to native images. Next: revisit any world, chase perfect scores, and go build something real in Java.