Project Leyden’s AOT Code Cache: How Java Is Solving Its Cold-Start Problem Without GraalVM
Leyden delivered its first three features in Java 24 and 25. A fourth lands in JDK 26. Here’s why this matters — and how it’s fundamentally different from GraalVM’s native image approach.
If you’ve ever deployed a Java service inside a container, you know the feeling. The pod spins up, Kubernetes marks it ready, and for the next five to thirty seconds — depending on your framework — it’s barely functional. Requests crawl, timeouts fire, the JIT hasn’t warmed up yet. Your p99 latency is embarrassing. That’s the JVM cold-start problem, and for a long time, the only real answer was GraalVM Native Image. That’s changing now, quietly and incrementally, through Project Leyden.
Leyden isn’t a single feature or a big bang release. Instead, it’s a research project inside OpenJDK that is shipping one JEP at a time — starting with Java 24, continuing through Java 25, and picking up again with JDK 26. Together, these JEPs build an ahead-of-time (AOT) cache that dramatically reduces both startup time and time-to-peak-performance. And unlike GraalVM Native Image, none of it requires you to give up the full JVM or rewrite a line of code.
1. Why Cold-Start Is Such a Hard Problem
To understand what Leyden is doing, it helps to understand what the JVM is doing on every single startup — because it’s a lot more than most developers realise.
When your application starts, the JVM has to read, parse, verify, load, and link every class your application uses. A typical Spring Boot application touches somewhere between 15,000 and 25,000 classes by the time it’s fully initialised. Each of those classes goes through multiple stages of processing before any of its code can actually run. Additionally, the JIT compiler — HotSpot — doesn’t compile methods to native code until it has seen them run enough times to bother. That observation phase takes time. The result is a two-headed problem: startup latency (the time before the first request is handled) and warmup latency (the time before the JIT has generated optimised native code for the hot paths).
Furthermore, the traditional solution — Class Data Sharing (CDS) — helps with parsing and reading, but it stops short of loading and linking. That gap is exactly what Leyden is closing, one JEP at a time.
Where JVM Startup Time Goes

2. The Four JEPs — A Delivery Timeline
Project Leyden has shipped four JEPs across three JDK releases. Each one builds on the last, and together they tell a coherent story about shifting work earlier — from the production run to a one-time training run.
2.1 JEP 483 — Ahead-of-Time Class Loading & Linking (JDK 24)
This was the first Leyden feature to land in mainline OpenJDK. JEP 483 extends Class Data Sharing to go beyond parsing. After a training run, the JVM stores classes in a fully loaded and linked state inside an .aot cache file. On subsequent starts, those classes are available immediately — no re-parsing, no re-verification, no re-linking. The result: Spring PetClinic starts 41% faster, with roughly 21,000 classes appearing already linked at application boot. No code changes required. Furthermore, no new constraints are introduced.
2.2 JEP 514 — Ahead-of-Time Command-Line Ergonomics (JDK 25)
JEP 514 is the quality-of-life release. JDK 24’s workflow required three separate commands: record, assemble, run. JEP 514 collapses the first two into one. Pass -XX:AOTCacheOutput=app.aot and the JVM both records the training observations and builds the cache on shutdown. Consequently, the workflow drops to two steps: train once, deploy with the cache. It also introduced a new JDK_AOT_VM_OPTIONS environment variable for fine-tuning the cache creation sub-process without polluting the production command line.
2.3 JEP 515 — Ahead-of-Time Method Profiling (JDK 25)
Where JEP 483 addressed startup, JEP 515 attacks warmup. During a training run, the JVM records which methods are called most frequently. Those profiling records are stored in the AOT cache. In production, the JIT compiler reads them immediately at boot, rather than waiting to collect its own profiles. As a result, HotSpot can begin compiling hot methods to native code far earlier. In practice, this means reaching peak throughput significantly faster than a standard JVM — without giving up the JIT’s ability to adapt at runtime.
2.4 JEP 516 — Ahead-of-Time Object Caching with Any GC (JDK 26)
The most technically sophisticated of the four, JEP 516 addresses a real architectural constraint that had been quietly limiting the previous features: the AOT cache stored Java objects (like loaded Class instances and their associated strings and arrays) in a GC-specific binary format, memory-mapped directly into the heap at startup. That approach is fast — but it requires the object layout to match the GC’s exact memory model. ZGC, which uses colored pointers to encode metadata directly into object references, uses a fundamentally incompatible layout. So all of the previous Leyden features simply didn’t work with ZGC at all.
JEP 516 replaces the format with a GC-agnostic streaming approach. Objects are materialised by a background thread at startup, one by one, using the Access API — meaning the GC can lay them out according to its own rules. As long as a spare CPU core is available, this background work doesn’t slow the startup process in practice. Additionally, the JDK now ships with a baseline AOT cache that works across all GC implementations, giving every Java application a free starting benefit even without a custom training run.
Startup Time Improvements Across Leyden JEPs

3. Using the AOT Cache in Practice
The workflow in JDK 25 and beyond is genuinely simple. You train once — ideally mimicking production as closely as possible — and deploy with the cache from then on. Here’s what that looks like end to end:
AOT cache workflow (JDK 25+)
# Step 1 — Training run: record observations + write the cache on shutdown
java -XX:AOTCacheOutput=app.aot \
-jar myapp.jar
# Step 2 — Production run: load the cache on every subsequent start
java -XX:AOTCache=app.aot \
-jar myapp.jar
Training run fidelity mattersThe training run should closely mirror production behaviour. The JVM will refuse to use a cache if the classpath, JVM flags, or key runtime conditions differ significantly from when it was built. Mock external dependencies to load the right classes, but avoid loading test-only code that won’t appear in production.
One important constraint to keep in mind: Leyden currently only caches classes loaded by the standard JDK class loaders. Applications that rely heavily on custom class loaders — a common pattern in some older OSGi or plugin architectures — won’t get full benefit. Frameworks like Quarkus have addressed this by introducing a dedicated aot-jar packaging that delegates all class loading to the standard loader transparently.
4. Leyden vs. GraalVM Native Image — The Real Difference
This is where the comparison gets interesting, because it’s tempting to think of these as two solutions to the same problem competing for the same use case. They’re not — not exactly.
GraalVM Native Image performs closed-world AOT compilation. It analyses your entire application at build time, compiles everything to native machine code, and produces a self-contained executable. The result is genuinely impressive: near-instant startup (often under 100ms), very low memory footprint, and a small container image. However, the closed-world assumption means that dynamic features — reflection, dynamic class loading, runtime proxies, serialisation — require explicit configuration to work. The native image build itself can take minutes. And there is no JIT at runtime, so the peak throughput of a warmed-up JVM can exceed native image performance under sustained load.
Leyden, by contrast, stays within the open-world JVM model. Your application runs on a real HotSpot JVM, with full dynamic capabilities intact. The AOT cache simply front-loads a portion of the work that would otherwise happen at startup, and seeds the JIT with prior observations. The JIT is still running. Dynamic class loading still works. Reflection still works — no configuration file needed. The tradeoff is that Leyden’s startup improvements, while substantial, won’t match native image on raw startup latency. The Spring PetClinic’s 41% improvement is impressive — but native image can achieve 90%+ reductions in startup time for the same app.
“Native image optimises for startup and footprint. Leyden optimises for the gap between ‘nothing works yet’ and ‘everything works well’ — without giving up the JVM you already know.”— Synthesis of Project Leyden goals, openjdk.org/projects/leyden
The honest answer is that both approaches will coexist, serving different deployment profiles. Short-lived functions and CLI tools are excellent native image candidates. Long-running services with complex dynamic behaviour — the majority of enterprise Java — are where Leyden shines, because it delivers meaningful improvement with zero code changes and zero compatibility risk.
| Dimension | GraalVM Native Image | Project Leyden (AOT Cache) |
|---|---|---|
| Code changes required | Sometimes (reflection config) | None |
| Full JVM features | Limited (closed-world) | Yes (open-world) |
| JIT compiler at runtime | No | Yes |
| Peak throughput ceiling | Below warmed JVM | Full JVM peak |
| Startup improvement | Very high (80–95%) | Significant (40–55%+) |
| Build time cost | High (minutes) | Low (one training run) |
| Container image size | Very small | JDK + cache file (~40–200 MB) |
| ZGC compatible | N/A (no GC) | Yes (from JDK 26) |
| Part of OpenJDK | No (separate distribution) | Yes |
5. What This Means for Containers and Serverless
The container and serverless use case is where Leyden’s design decisions feel most deliberate. In a horizontally scaled deployment, you build the AOT cache once — during your CI pipeline — and ship it as part of your container image. Every pod that spins up loads the same cache file. The training overhead is paid once; every deployment run benefits.
Moreover, because the cache is tied to the classpath and JVM flags, it integrates naturally with Docker and OCI build pipelines. A typical Dockerfile adds one layer for the training run, writes the .aot file, and the final image includes both the jar and the cache. The net result is that your Kubernetes pods reach a useful state significantly faster — with no changes to your application code, your framework configuration, or your deployment tooling.
Framework support in 2025–2026Spring Boot 3.3+ supports Leyden AOT caching out of the box. Quarkus has built dedicated
aot-jarpackaging. Micronaut support is also in progress. If you’re on a modern version of any major Java framework, you’re likely already in a position to benefit with minimal effort.
For serverless specifically, the picture is more nuanced. AWS Lambda and similar platforms cold-start every function invocation after a period of inactivity. Leyden’s cache file would need to be bundled into the deployment package and persist across invocations — which is possible, but requires the execution environment to support it. In practice, CRaC (Coordinated Restore at Checkpoint) remains a stronger fit for serverless, since it restores from a full memory snapshot. That said, Leyden’s broader compatibility story — working on any OS, any GC, without security-sensitive memory snapshots — makes it a more attractive default for teams not specifically chasing sub-100ms startup.
6. What Comes Next for Leyden
The roadmap is genuinely exciting. The Leyden premain branch — the experimental prototype that runs ahead of what’s in mainline — currently includes two features not yet in any released JDK: Ahead-of-Time Code Compilation, which stores pre-compiled native method code in the cache (so methods can execute natively from the very first startup with no JIT delay at all), and Ahead-of-Time Dynamic Proxy Generation, which pre-generates the reflective proxies that frameworks like Spring use extensively. Both features are enabled by default when building a cache from the premain branch and show further meaningful startup reductions in early benchmarks.
Furthermore, as the JDK now ships with a baseline AOT cache covering JDK classes — thanks to JEP 516 — even applications that skip the custom training run will see a small baseline improvement from JDK 26 onwards. That’s a significant quality-of-life win for the ecosystem as a whole.
7. What We’ve Learned
Project Leyden is a methodical, backwards-compatible answer to Java’s cold-start problem — delivered as a series of JEPs that each shift a specific category of work from production runtime to a one-time training run. JEP 483 (JDK 24) moved class loading and linking. JEP 514 and JEP 515 (JDK 25) simplified the workflow and added method profiling for faster JIT warmup. JEP 516 (JDK 26) made the cache GC-agnostic, unlocking ZGC support and shipping a baseline cache with the JDK itself.
Unlike GraalVM Native Image, Leyden does not require code changes, closed-world analysis, or sacrificing the JIT. The startup gains (~41% for Spring PetClinic) are meaningful for container deployments, and the warmup gains are significant for any service that cares about p99 latency during scale-out. The comparison isn’t “Leyden vs. Native Image” — it’s “Leyden for JVM workloads, Native Image for startup-critical, footprint-sensitive deployments.” Both will matter.
If you’re running JDK 25 today, you can start experimenting with AOT caching in two commands. If you’re on JDK 26, you’re already benefiting from the baseline cache on every startup. The cold-start problem isn’t solved yet — but it’s being systematically dismantled.
Key Links
- Project Leyden homepage
- Premain branch (GitHub)
- leyden-dev mailing list
- AOT Cache guide — inside.java
- Quarkus + Leyden integration


