AOT, Leyden and native images
CDS, the AOT cache, and starting fast.
- chapter
- chapter 13.12
- reading time
- 8 min read
a JVM starts slow because every run redoes the same work: find, parse, verify and link the same classes, then warm up the same methods. CDS saves the parsed classes to a file and maps them in. java 24's AOT cache (Project Leyden) goes further: classes already loaded and linked, and since 25, method profiles from a training run too.
one extra command, no code changes, and on the app in this chapter startup went from 67 ms to 43 ms. a native image goes all the way (no JVM at startup at all), at the price of a closed world.
the picture
you know how every java -jar seems to spend its first moments doing nothing useful? it's not nothing: it's loading and linking the same thousand-odd classes as last time, and the time before that.
think of a restaurant that preps its kitchen from scratch every single morning: unpacking the same deliveries, chopping the same onions, laying out the same stations, before the first order. CDS is keeping the pantry stocked overnight (the classes are already unpacked). the AOT cache is coming in to a kitchen where the stations are already set up, because someone did a practice service last night and wrote down exactly what was used (the training run). a native image is a food truck that only sells what was cooked this morning: open instantly, but nothing off-menu.
what the cache can hold
each java release adds a layer. pick a version and click a layer.
the AOT cache: classes are not just parsed but already loaded and linked, with constant pool entries resolved, as if the training run had just happened. lambdas and other hidden classes included.
available in java 25.
try it on jdk 25
no special build step, no code changes. here's a small report app (parses some CSV, groups with streams, pattern-matches over a sealed interface, formats dates, builds an HTTP request) packaged as app.jar. it loads 1,238 classes on startup.
$ java -XX:AOTCacheOutput=app.aot -jar app.jar
report for 1 Sep 2025: 6 sales, {east=301.8, north=126.0, south=79.5}, grand total 568.68 (POST example.com)
Launching child process .../bin/java to assemble AOT cache app.aot using configuration app.aot.config
Picked up JAVA_TOOL_OPTIONS: -Djava.class.path=app.jar -XX:AOTCacheOutput=app.aot -XX:AOTConfiguration=app.aot.config -XX:AOTMode=create
Reading AOTConfiguration app.aot.config and writing AOTCache app.aot
AOTCache creation is complete: app.aot 14254080 bytes$ java -XX:AOTCache=app.aot -jar app.jarthat's it. the training run is a normal run of your app that also records what it loaded and linked (and its method profiles); then a child JVM writes the 14 MB cache. ask -Xlog:class+load how many classes still had to be loaded the slow way:
| run | classes not from an archive |
|---|---|
| default (only the JDK's CDS archive) | 137 of 1,238 |
with -XX:AOTCache=app.aot | 2 of 1,238 |
and the wall time, start to exit:
notice hello world doesn't improve with an AOT cache: its 415 classes are almost all in the JDK's default CDS archive already, which is on by default and already saves 40% (49 → 30 ms). the AOT cache pays off as the app's class graph grows: 35% faster for this tiny app. JEP 483 reports Spring PetClinic (about 21,000 classes) starting in 2.6 s instead of 4.5 s with a 130 MB cache, 42% faster.
native images
GraalVM's native-image takes the idea to its end. it compiles your app and the parts of the JDK it uses into a single native executable at build time, under a closed-world assumption: everything that can ever run must be known when you build. no class loading at runtime, no bytecode, no JIT warmup. startup is typically milliseconds and memory use drops a lot.
the price is the closed world. reflection, dynamic proxies, resources and Class.forName need configuration (or a framework that generates it), builds take minutes, and without profile-guided optimization peak performance usually trails a warmed-up C2. frameworks like Quarkus, Micronaut and Spring Boot have invested heavily in making that configuration automatic.
| CDS / AOT cache | native image | |
|---|---|---|
| startup | faster (43 vs 67 ms here) | much faster (no JVM boot) |
| peak performance | full C2, same as always | AOT-compiled, usually lower without PGO |
| dynamic java (reflection, class loading, agents) | everything works | needs build-time configuration |
| build step | one training run | a full native compile |
| standard JDK | yes, java 24+ | no, GraalVM toolchain |
- every JVM start reloads and relinks the same classes. archives skip that work.
- CDS stores parsed classes and is on by default. the AOT cache (java 24) stores loaded and linked ones.
- java 25: one command to train and build (JEP 514), plus method profiles (JEP 515).
- here: 67 ms → 43 ms. 137 classes loaded the slow way → 2.
- a mismatched cache can make startup slower. build it like production, check with -XX:AOTMode=on.