skip to content
everything.jvm.aotAndLeyden

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.

java =
what java 25 can archive (bottom layer first)
JEP 483
loaded + linked classes

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.

the layers a startup archive can hold, by java release. green means available in the version you picked. JEP numbers and releases from openjdk.org; 28 is still in development, so JEP 544 is "targeted", not final.

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.

training run + cache, one command (JEP 514)
$ 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
every run after that
$ java -XX:AOTCache=app.aot -jar app.jar

that'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:

runclasses not from an archive
default (only the JDK's CDS archive)137 of 1,238
with -XX:AOTCache=app.aot2 of 1,238

and the wall time, start to exit:

report · -Xshare:off (no CDS)104 ms
report · default CDS (jdk classes)67.1 ms
report · dynamic AppCDS57.1 ms
report · AOT cache43.4 ms
hello world · -Xshare:off49.3 ms
hello world · default CDS29.7 ms
hello world · AOT cache29.6 ms
median of 61 runs each, jdk 25 (temurin 25.0.4.1), M4 Pro, 14 cores, measured while the machine was busy with other work (load average ~7.6), so the absolute numbers are a little high. the ratios held across repeat runs.

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.

quick check
what does the training run actually need to do?

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 cachenative image
startupfaster (43 vs 67 ms here)much faster (no JVM boot)
peak performancefull C2, same as alwaysAOT-compiled, usually lower without PGO
dynamic java (reflection, class loading, agents)everything worksneeds build-time configuration
build stepone training runa full native compile
standard JDKyes, java 24+no, GraalVM toolchain
keylines
  • 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.