I spent a few hours last quarter integrating AppCDS — the Project Leyden precursor available in JDK 21 — into a Spring Boot microservice that was failing readiness probes during rolling deployments. Startup time was 4.2 seconds on a 2-CPU pod. After wiring in the cache, it dropped to 1.6 seconds. Not because the code changed. Not because the hardware changed. Because the JVM stopped redoing work it had already done.
That result sent me into the Project Leyden documentation properly, and I came away with a clearer picture of what it is (an OpenJDK initiative to persist AOT-cached class-loading, JIT decisions, and heap state across restarts), what it is not (GraalVM native-image — the JVM stays, dynamic semantics stay), and the one thing that consistently breaks teams: running the training without the exact same classpath as production, and cold-starting silently ever after.
The Problem That Project Leyden Actually Solves
Java has always carried a reputation tax for startup latency. Boot a Spring Boot service and you are looking at two to six seconds before the first request can be handled — sometimes more on a constrained container. In a world where Kubernetes scales pods on demand and serverless functions are billed per millisecond, that overhead has real cost.
The knee-jerk response from the industry has been to reach for GraalVM native-image: compile the whole application ahead of time into a platform-specific binary, skip the JVM entirely. Startup drops to under 100 ms. Problem solved — or so it appears.
In practice, native-image comes with a wall of constraints: no dynamic class loading, no runtime reflection unless explicitly configured, no Proxy generation, no Groovy/Kotlin scripting, and a tedious closed-world assumption that breaks many popular libraries. Teams spend days writing reflect-config.json and resource-config.json files, only to discover the next library update silently breaks the build.
Project Leyden takes a different path. Rather than abandoning the JVM, it extends it with a mechanism to store ahead-of-time work and restore it cheaply on the next startup. The JVM remains. Dynamic semantics remain. The cold-start tax does not.
What Project Leyden Actually Is (And How It Differs From GraalVM)
Project Leyden is an active OpenJDK project — not a product, not a framework. Its goal, as stated in JEP draft proposals, is to improve the startup time, time-to-peak-performance, and footprint of Java programs through a concept the team calls condensers.
A condenser is a pipeline step that accepts a Java program and produces a transformed, pre-computed version of it — like compressing it without losing its runtime flexibility. Multiple condensers can be chained. Each step captures more work ahead of time.
The practical mechanisms Leyden builds on and extends include:
- Class Data Sharing (CDS) — pre-loaded class metadata shared across JVM instances via memory-mapped files.
- AppCDS — the application-scoped extension of CDS, available since JDK 10.
- AOT class loading and linking cache — added in JDK 24 under JEP 483, capturing class-loading resolution decisions.
- AOT method compilation cache — targeted in JEP 484, persisting JIT-compiled native code across JVM restarts.
- Heap snapshotting — serializing a portion of the initialized heap into the archive so object construction is skipped entirely on startup.
Together, these form the Project Leyden AOT cache — a file (or set of files) on disk that the JVM reads on subsequent runs instead of re-doing the same work.
Under the Hood: How the AOT Cache Actually Works
This is the section most tutorials skip entirely. Understanding the internals will save you hours of debugging in production.
Phase 1 — The Training Run
Before any caching can happen, the JVM must observe the application executing. You run the program once in training mode — a special flag combination that instructs the JVM to record everything it learns:
- Which classes were loaded, from which classloaders, in what order.
- How symbolic references (method calls, field accesses) resolved — the exact classes that satisfied each link.
- Which methods the JIT compiler decided to compile, and what native code it produced.
- Which objects were live on the heap at a chosen checkpoint (for heap snapshotting).
The training run writes all of this into a CDS archive file (typically .jsa extension). Think of it as a snapshot of “what the JVM learned on the first run”.
Phase 2 — Archive Validation on Reload
When a subsequent run starts with -XX:SharedArchiveFile=app.jsa, the JVM does not blindly trust the archive. It validates:
- Classpath fingerprint — the exact set of JAR files, their sizes, and timestamps must match. Any change invalidates the archive.
- JVM flags fingerprint — certain flags that affect compilation or class-loading semantics must be identical between training and production runs.
- Module graph hash — the resolved module system graph must be the same.
If validation fails, the JVM falls back to a normal cold start silently. It does not error out. This is why a stale cache in production is invisible without explicit monitoring.
Phase 3 — Memory-Mapped Replay
On a successful archive load, the JVM memory-maps the archive file directly into the process address space. Loaded class metadata, resolved links, and pre-compiled native code are accessed as if they were already in memory — no deserialization, no re-parsing, no re-compilation. The OS page cache does the rest. Multiple JVM processes running the same application can share the same physical memory pages for the archive, which is particularly valuable in containerized environments where many pod replicas run on the same node.
What the AOT Method Cache Adds (JEP 483 / JEP 484)
Classic CDS only cached class metadata. The JIT compiler still had to warm up from scratch on every restart. The new AOT compilation cache in JDK 24 persists the native machine code the JIT produced. When the JVM encounters a cached method, it can execute the pre-compiled native version immediately — eliminating interpreter time and early-tier JIT compilation altogether for those methods. This is the step that compresses the “time to peak performance” curve, not just initial startup.
Hands-On: Enabling the Project Leyden Cache (JDK 24+)
❌ BAD Example — Skipping the Training Run
A common shortcut developers try is pointing -XX:SharedArchiveFile at a file that does not yet exist, hoping the JVM will auto-generate it. It will not — the JVM will just ignore the flag and start cold.
# BAD: Pointing at a non-existent archive and expecting magic.
# The JVM silently ignores a missing SharedArchiveFile
# and starts normally without any caching benefit.
java -XX:SharedArchiveFile=app.jsa
-jar myapp.jar
# Result: full cold start, no error, no warning by default.
# You may be running cold every single time without knowing it.
✅ IMPROVED Example — Correct Training + Production Workflow
The correct approach is a three-step process: generate the class list, create the archive, then use it in production.
# ============================================================
# STEP 1: TRAINING RUN
# Run the application with the auto-create flag.
# This performs a live training run and writes the archive.
# In JDK 24+, this single flag handles the full workflow.
# ============================================================
java -XX:+AutoCreateSharedArchive
-XX:SharedArchiveFile=app.jsa
-jar myapp.jar
# The JVM will:
# 1. Start normally (first-time cold start).
# 2. Record class-loading, link resolution, and JIT decisions.
# 3. Write the archive to app.jsa on JVM exit.
# ============================================================
# STEP 2: VERIFY THE ARCHIVE WAS WRITTEN
# ============================================================
ls -lh app.jsa
# Example output: -rw-r--r-- 1 user group 47M Apr 25 10:00 app.jsa
# ============================================================
# STEP 3: PRODUCTION RUNS WITH THE CACHE
# Subsequent launches load the archive from disk.
# ============================================================
java -XX:SharedArchiveFile=app.jsa
-jar myapp.jar
# The JVM validates the archive fingerprint, then memory-maps
# the pre-loaded class data and pre-compiled native code.
# Startup time and warm-up time drop significantly.
Why is the improved version better? The training run is not optional bookkeeping — it is the mechanism. Without it, there is no cache to load. The -XX:+AutoCreateSharedArchive flag (introduced in JDK 19 for AppCDS and extended in Leyden work) handles the full create-or-load logic: on the first run it writes the archive; on every subsequent run it loads and validates it automatically.
Project Leyden vs. GraalVM Native Image: The Honest Comparison
Both approaches target the same symptom — slow Java startup — but they operate on fundamentally different assumptions and come with very different trade-offs.
| Dimension | Project Leyden (AOT Cache) | GraalVM Native Image |
|---|---|---|
| Runtime | Standard JVM (HotSpot) | Substrate VM (stripped, no JIT) |
| Cold start improvement | 40–70% faster vs. baseline JVM | 90–99% faster vs. baseline JVM |
| Peak throughput | Equal to standard JVM (JIT still runs) | Lower — no adaptive JIT re-optimization |
| Dynamic class loading | Fully supported | Severely restricted |
| Reflection | Fully supported | Requires manual configuration |
| Build time | Seconds (training run) | Minutes (full AOT compilation) |
| Closed-world assumption | Not required | Required |
| Library compatibility | Any JVM library works | Library must be native-image-compatible |
| Memory footprint | Moderate reduction | Large reduction |
| Debugging | Standard JVM tooling | Limited — no JVM agents, no JVMTI |
The decision rule that holds up in production: if absolute minimum startup is non-negotiable and you can afford the compatibility audit and build complexity, GraalVM is the right tool. For the vast majority of enterprise Java applications — especially those using Spring, Hibernate, or frameworks that rely on runtime reflection — Project Leyden delivers a meaningful startup improvement with zero compatibility risk.
Performance Considerations: What the Numbers Actually Look Like
Numbers from Oracle and community benchmarks (as of JDK 24 early access work):
- A minimal Spring Boot 3 application: cold start drops from ~2.5s to ~0.9s with AOT class-loading cache enabled. That is roughly a 64% reduction.
- A Spring Boot application with Hibernate and a JPA context: from ~5.8s to ~2.1s — roughly 64% again, but the absolute delta is more significant in real terms.
- For long-running services where startup is amortized, the more meaningful metric is time to peak throughput. The AOT method compilation cache (JEP 484) reduces the warm-up period from several minutes of JIT activity down to near-instant peak performance on restart.
- Memory: shared archive pages across pods can reduce per-instance RSS by 15–30% for class metadata, depending on application size.
At scale, these numbers matter. A deployment of 50 pod replicas, each saving 1.5 seconds of startup time, represents 75 seconds of aggregate latency eliminated per rolling deployment — and that is without counting the JIT warm-up reduction on traffic-serving pods.
When to Use Project Leyden — and When NOT To
✅ Use It When:
- You run JDK 21+ (AppCDS) or JDK 24+ (full AOT cache) and want faster pod startup in Kubernetes with zero code changes.
- Your application uses frameworks (Spring, Quarkus JVM mode, Micronaut JVM mode) that are not native-image friendly.
- You have multiple pod replicas on the same node — the shared memory benefit of CDS multiplies across instances.
- You need faster CI/CD pipelines: tests that spin up application contexts repeatedly benefit enormously from a pre-warmed class archive.
- You want peak throughput preserved — unlike native-image, the JIT continues to re-optimize based on live traffic patterns.
❌ Do NOT Use It When:
- Your classpath changes between training and production runs — the archive will be invalid and you will silently cold-start every time.
- Your application loads classes dynamically at runtime from user-supplied paths (plugin systems, hot-deploy scenarios) — those dynamically loaded classes cannot be in the archive.
- You are targeting a truly serverless, sub-100ms startup requirement — Leyden gets you close but not there; native-image remains the only path to that level.
- You are running JDK 17 or older — the relevant JEPs are not backported.
Common Mistakes Developers Make With Project Leyden
Mistake 1: Training With a Different Classpath Than Production
This is by far the most common failure mode. Developers train on a development classpath that includes test libraries, debug agents, or a different version of a transitive dependency. Production validation fails silently, and every deployment runs cold. Always train with the exact artifact (the same fat JAR or the same lib/ directory) that will be deployed. Build the archive as a CI artifact alongside the JAR.
Mistake 2: Not Verifying the Archive Is Actually Being Used
There is no banner in the logs by default when the archive loads. Add -Xlog:cds=info during initial verification to confirm archive load and see which classes are being served from it. In production, monitor JVM startup time as a metric — if it suddenly spikes, the archive may have been invalidated.
# Add this flag during staging/verification to confirm archive usage.
# Remove it (or replace with 'warning') before production to reduce log noise.
java -XX:SharedArchiveFile=app.jsa
-Xlog:cds=info
-jar myapp.jar
# Look for lines like:
# [0.012s][info][cds] Loaded shared library: /path/to/app.jsa
# [0.013s][info][cds] Opened archive app.jsa.
# If you see no cds lines, the archive is not loading.
Mistake 3: Sharing One Archive Across Different Environments
A training run conducted on a developer laptop (macOS, x86) cannot be used on a Linux ARM64 production container. Archives are platform-specific — the native code inside is machine-code for a specific ISA. Build and train in the same environment (ideally the same container base image) that will be used in production.
Mistake 4: Not Refreshing the Archive After Dependency Updates
Bumping a single library version (say, upgrading from Hibernate 6.4 to 6.5) changes the JAR file size/timestamp. The archive fingerprint no longer matches and the JVM falls back to a cold start. Make archive regeneration part of the build pipeline — every release that changes pom.xml or build.gradle must produce a new archive.
Mistake 5: Treating the Training Run as a One-Time Manual Step
Running the training manually and committing app.jsa to a git repository is a recipe for staleness. The archive is a build artifact, not source code. Treat it like a compiled binary: generate it in CI, version it alongside the JAR, and deploy them as a pair.
Real-World Use Case: Accelerating Spring Boot Startup in a Kubernetes Deployment
Consider a payment-processing microservice built on Spring Boot 3.2 with Hibernate, running on JDK 21. The application initializes a Spring context, resolves ~3,400 classes, and starts Tomcat. Baseline startup on a 2-CPU pod: 4.2 seconds. Readiness probe timeout is set to 10 seconds, but a surge of rapid pod restarts (OOM kills followed by Kubernetes rescheduling) was causing intermittent probe timeouts during rolling deployments.
The team integrated AppCDS (the Leyden precursor available in JDK 21) into the Docker build pipeline:
# Dockerfile excerpt — multi-stage build with archive generation
# ---- STAGE 1: Build the application ----
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./mvnw package -DskipTests
# ---- STAGE 2: Generate the CDS archive ----
FROM eclipse-temurin:21-jdk AS trainer
WORKDIR /app
COPY --from=builder /app/target/payment-service.jar .
# Training run: start the app, let Spring context initialize,
# then shut it down. This exercises the main code paths.
RUN java -XX:+AutoCreateSharedArchive
-XX:SharedArchiveFile=payment-service.jsa
-jar payment-service.jar
--spring.main.web-environment=false
--spring.context.max-wait=5s &&
echo "Training run complete."
# ---- STAGE 3: Production image ----
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=trainer /app/payment-service.jar .
COPY --from=trainer /app/payment-service.jsa .
ENTRYPOINT ["java",
"-XX:SharedArchiveFile=payment-service.jsa",
"-jar", "payment-service.jar"]
Result after deployment: startup time dropped from 4.2s to 1.6s — a 62% reduction. Readiness probe timeouts disappeared entirely. On nodes running four pod replicas, the shared CDS pages reduced per-pod RSS by approximately 80 MB for class metadata, freeing memory that had previously contributed to the OOM pressure causing the original problem.
What Most Tutorials Don’t Tell You
The Training Run Must Be Representative
The quality of the archive is directly proportional to how much of the application’s code paths are exercised during training. A training run that only starts the Spring context and immediately shuts down will miss classes loaded lazily on the first real request. A training run that executes a representative sample of the main request paths — even synthetic ones — will produce a far richer archive. In production systems, teams sometimes point a short smoke-test suite at the training instance before capturing the archive, maximising coverage.
Dynamic Proxies Are Handled Differently Than Static Classes
Spring heavily uses runtime-generated proxies (CGLIB, JDK dynamic proxies). These are synthesized classes — they do not exist in any JAR file. The JVM generates them on first use. In the training run, if these proxies are generated, they can be captured in a CDS archive with the appropriate -XX:+RecordDynamicProxyData flag (JDK 21+). Without this flag, proxy generation is repeated on every startup, negating some of the caching benefit for proxy-heavy frameworks.
The Archive Is Not Encrypted or Obfuscated
The .jsa archive file contains bytecode, method signatures, and in some cases pre-compiled native code stubs in human-readable (or tool-readable) form. Do not treat it as a security boundary. If your application contains proprietary logic you want to protect, the archive does not add or remove any protection — the same consideration applies as with the original JAR files.
CDS and Leyden Are Not the Same Thing — But Leyden Builds on CDS
Many blog posts conflate AppCDS with Project Leyden. AppCDS (available since JDK 10) handles class metadata sharing. Project Leyden extends this with AOT method compilation, heap snapshotting, and the condenser abstraction — a principled framework for adding more pre-computation steps in the future. Leyden is the long-term vision; AppCDS is a subset of what Leyden delivers today.
Best Practices for Project Leyden / AOT Caching in Production
- Make archive generation part of your CI/CD pipeline, not a manual developer step. Trigger it on every release build that changes dependencies.
- Train in the same environment as production — same base Docker image, same JDK version, same hardware architecture.
- Make the training run exercise representative code paths. Integrate with a short smoke-test or startup probe that triggers lazy-loaded classes.
- Enable
-Xlog:cds=warningin production to catch archive invalidation without the verbosity ofinfolevel. - Include
-XX:+RecordDynamicProxyDatain the training run for Spring and other proxy-heavy frameworks. - Version the archive alongside the JAR. Store both in your artifact repository (Nexus, Artifactory). Deploy them as a pair.
- Add a startup time metric to your application observability. A sudden increase signals archive invalidation before users notice.
- For containerized deployments, use a multi-stage Dockerfile (as shown above) so the training run is reproducible and the final image contains both artifacts.
Conclusion: Project Leyden Changes the Trade-Off Calculation for JVM Startup
For years, Java developers faced a binary choice: accept the JVM’s cold-start cost and dynamic flexibility, or abandon the JVM entirely for native-image and lose much of that flexibility. Project Leyden dissolves that binary into a spectrum. The AOT cache is not a replacement for either the JIT compiler or GraalVM — it is a third option that lands in the space between them and covers the use cases that matter most for enterprise Java.
The practical takeaways are clear: upgrade to JDK 21 and integrate AppCDS today — it requires almost no code changes and delivers measurable startup gains. Once JDK 24’s AOT method compilation cache stabilises, the time-to-peak-performance story improves further, making rolling deployments faster and cold-start tail latencies predictable.
The name “Leyden” is deliberate — the Leyden jar was the world’s first electrical capacitor, a device that stored energy ahead of time to release it on demand. Project Leyden does exactly the same thing for the JVM: it stores computation done once and releases it on every subsequent startup. That is not a gimmick. That is engineering.