As you may know, Project Leyden has already delivered several JEPs starting in JDK 28:
- JEP-483 - Ahead-of-Time Class Loading & Linking
- JEP-514 - Ahead-of-Time Command-Line Ergonomics
- JEP-515 - Ahead-of-Time Method Profiling
- JEP-516 - Ahead-of-Time Object Caching with Any GC
Until recently, however, one key proposal remained on the roadmap:
- JEP-544 - Ahead-of-Time Code Compilation
In this article, I explain what JEP-544 is, what benefits it brings to the JDK, and how it performs in practice using real benchmarks.
Theory
The core philosophy behind Project Leyden is shifting work from production runs to a training run, allowing subsequent production runs to benefit from the pre-generated artifacts. To achieve this, the JVM creates an Ahead-of-Time (AOT) cache. Prior to JDK 28, this cache stored information about linked classes and method profiling data. JEP-544 extends this capability by allowing code compiled by the JIT compiler to be cached as well.
It is worth examining why this addition is so significant.
Upon startup, the JVM executes application code using the interpreter. This approach enables immediate execution, though the resulting machine code is far from optimal. From the moment the application starts, the JVM continuously profiles method execution. When a method becomes hot, the JVM compiles it using the JIT compiler. JIT compilation operates across two main tiers:
- C1 - Lightly optimized code with instrumentation to collect additional profiling data.
- C2 - Heavily optimized machine code.
If you want to read more about that, see my previous article on this topic.
Crucially, the JIT compiler competes with application logic for system resources. This contention is particularly noticeable during startup and initial warmup when hundreds of methods require compilation simultaneously. You can observe this activity firsthand using the following command (requires JDK 25+):
Even for a simple Hello, World! program, the compilation output is extensive. Here is a brief snippet:
Before printing Hello, World!, the JIT compiler executes a substantial amount of work (while you can disable the JIT compiler entirely, doing so renders JEP-544 irrelevant). With AOT code caching, these methods are loaded directly from the AOT cache instead of being compiled at runtime.
Workflow & Usage
Using JEP-544 requires no changes to the existing Project Leyden workflow. To generate an AOT cache with compiled code, run:
To restrict the AOT cache to class linking and profiling data while disabling code caching, create the cache with these flags:
Using the generated cache remains identical in both cases:
Returning to our Hello, World! example, the tail of the compilation log now looks like this:
Two details stand out:
- Hello, World! prints around the 192ms mark (first column), compared to 554ms without the AOT cache.
- Several log entries now feature an A flag, confirming those methods were loaded directly from the AOT cache.
Current limitations
A few important constraints currently apply to AOT code caching:
- Support is currently limited to aarch64 and amd64 architectures.
- The training run and production run must execute on hardware with identical CPU features (e.g., if the training run uses AVX-512 vector instructions on amd64, production runs will require those same instructions).
- Both runs must use the same Garbage Collector, as compiled code contains GC-specific optimizations. (With G1 becoming the default GC across environments since JDK 27 via JEP-523, matching GC configurations has become simpler, but this requirement remains vital).
Practice
To evaluate this feature, I used a simple-application previously designed to compare JVM startup optimization strategies. To simulate modern cloud-native environments, where startup improvements matter most, I created Dockerfiles and then ran containers using the --cpus flag to mimic Kubernetes vCPU constraints.
Benchmarked Configurations
I evaluated four configurations:
- Baseline: Standard application execution.
- AOT Cache (Code Caching Enabled): Full Project Leyden cache including JIT-compiled code.
- AOT Cache (Code Caching Disabled): Project Leyden cache restricted to class loading, linking, and profiling metadata.
- CRaC Checkpoint: An application run restored from a CRaC checkpoint (the primary alternative to Project Leyden).
Each configuration was benchmarked under two resource allocations:
- 1 vCPU
- 4 vCPU
Artifact Size Comparison
Building dedicated Docker images for each vCPU count revealed notable differences in artifact sizes across solutions:
- AOT with Code Caching:
- AOT without Code Caching:
- CRaC:
In Project Leyden, disabling code caching reduces the total cache size. However, varying the allocated vCPU count has almost no effect on the final file size. Consequently, Leyden AOT caches are decoupled from vCPU count. A cache trained under 4 vCPUs runs seamlessly in a container limited to 1 vCPU, and vice versa.
By contrast, CRaC checkpoints capture a snapshot of process memory. Application settings derived from the host's core count, such as internal thread pool sizes, are frozen into the image. As a result, the 4-vCPU CRaC image is roughly 37% larger than its 1-vCPU counterpart.
Please note that cross-deploying CRaC images across different vCPU allocations can cause runtime inefficiencies. A checkpoint created on 1 vCPU will default to a single thread. When deployed to a 4-vCPU node, it will fail to fully utilize available hardware. Conversely, a 4-vCPU checkpoint deployed to a single vCPU will suffer from thread contention.
Test Environment
All benchmarks were conducted on the following environment:
- Host: 8 vCPUs (4 cores) - Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz, 32GB RAM, running Debian GNU/Linux 13 (trixie)
- Baseline & Leyden JDK: OpenJDK 64-Bit Server VM (build 28-ea+20-1588, mixed mode, sharing)
- CRaC JDK: OpenJDK 64-Bit Server VM Zulu27.28+101-CRaC-CA (build 27+35, mixed mode, sharing)
- Garbage Collector: G1 (default across all tested JDK versions)
Containers were launched using standard runtime parameters, such as:
The measurements include container startup overhead, which added a consistent baseline delay of approximately 100ms across all runs.

At 4 vCPUs, CRaC proved to be the fastest option, achieving roughly a 61% reduction in startup time compared to the baseline. However, Project Leyden with code caching performed competitively, delivering a 47% speedup. Enabling AOT code caching provided a noticeable gain over standard AOT caching without code compilation (47% vs. 38% improvement).
Results: 1 vCPU

Under a 1-vCPU constraint, the performance gap shifted significantly. CRaC maintained near-constant startup timing regardless of core count, yielding an 85% speedup over the baseline.
In contrast, Leyden’s AOT cache without code compilation provided only a 13% improvement, because the JIT compiler and application logic actively competed for a single vCPU during startup. Enabling AOT code caching eliminated much of this contention, pushing Project Leyden's overall startup speedup to 51%.
Conclusion
JEP-544 represents a major milestone for Project Leyden. As the benchmarks show, caching JIT-compiled code delivers tangible reductions in startup latency while accelerating application warmup times.
While CRaC remains hard to beat in absolute startup speed, Project Leyden offers a far more seamless developer experience. It eliminates the need for OS-specific restrictions and external, non-JDK tooling, works natively across standard JDK distributions, and simplifies deployment pipelines down to adding a few JVM flags for training and production runs.
Reviewed by Szymon Winiarz

