Project Leyden & JDK 26: Bringing AOT Caching to ZGC

Project Leyden is an ongoing initiative aimed at improving the startup and warmup times of the Java Virtual Machine (JVM). Building on the foundations laid by JEP 483, JEP 514, and JEP 515, the upcoming JDK 26 release will introduce JEP 516: Ahead-of-Time (AOT) Object Caching with Any GC. In this article, I explain the specific problems this feature addresses.
The problem
Until now, Project Leyden did not support the Z Garbage Collector (ZGC). ZGC is a low-latency collector designed to keep application pause times under a millisecond, even when managing massive heaps of up to 16TB.
The incompatibility stems from how ZGC handles object references compared to other GC implementations. ZGC uses a technique called colored pointers, where it encodes metadata bits directly into the object reference. This allows the collector to perform its work concurrently while the application is running.
Previous iterations of the AOT cache stored object references as literal memory addresses. Because ZGC's unique reference format differs so fundamentally from standard pointers, the existing AOT cache was incompatible.
The solution
To bridge this gap, JEP 516 introduces the ability to store object references as logical indices rather than physical memory addresses. At application startup, the JVM reads these objects from the cache and streams them into memory, remapping the indices to the correct memory addresses for the current environment.
This introduces a distinction between two types of caches:
- GC-specific cache: maps cached objects directly into memory
- GC-agnostic cache: streams objects into memory
The JVM uses heuristics to determine which format to use during the training run:
- GC-specific cache is used if
-XX:+UseCompressedOopsis enabled (the default for heaps under 32GB). The assumption here is that the production environment will mirror the training environment - specifically, that the heap will remain under 32GB and ZGC will not be used. - GC-agnostic cache is automatically triggered if any of the following conditions are met during training:
- ZGC is active
-XX:-UseCompressedOopsis explicitly set- The heap size exceeds 32GB
You can also manually force the creation of a streamable cache even when CompressedOops is on by using: -XX:+AOTStreamableObjects (it must be preceded by the-XX:+UnlockDiagnosticVMOptions option).
The UseCompressedOops (Ordinary Object Pointers) option determines the size of object references: 32-bit when enabled and 64-bit when disabled. Since ZGC requires 64-bit (uncompressed) pointers to store its metadata, it is fundamentally incompatible with the 32-bit GC-specific cache.
Because of this new GC-agnostic format, you can train a cache with one collector and deploy it with another. For example, you can create a cache using the default G1GC (while disabling compressed pointers):
java -XX:AOTCacheOutput=cache.aot -XX:-UseCompressedOops -jar example.jar
And then seamlessly run that same application using ZGC:
java -XX:AOTCache=cache.aot -XX:+UseZGC -jar example.jar
Testing
To evaluate the difference between memory-mapped (GC-specific) and streamable (GC-agnostic) AOT caches, I conducted tests on a Google Cloud Platform (GCP) VM running Debian GNU/Linux 12 (bookworm) on an Intel Xeon @ 2.20GHz (4 vCPUs, 16GB RAM).
For these tests, I used the latest (as of 31.01.2026) early-access build of OpenJDK Runtime Environment (build 26-ea+33-2879) and the Spring Petclinic sample application as a benchmark candidate.
I generated two distinct caches:
- GC-specific (memory-mapped)
java -XX:AOTCacheOutput=mapped/cache.aot -jar spring-petclinic-4.0.0-SNAPSHOT.jar
- GC-agnostic (streamable)
java -XX:+UnlockDiagnosticVMOptions -XX:AOTCacheOutput=streamable-compressed/cache.aot -XX:+AOTStreamableObjects -jar spring-petclinic-4.0.0-SNAPSHOT.jar
And perform measurements using a simple-benchmark application which I created some time ago to make a comparison between different approaches to speed up JVM-based application startup time.
This are the results:

They show that for this particular use case we get:
- ~28% faster startup when using GC-specific cache
- ~26% faster startup when using GC-agnostic cache
So while a more flexible GC-agnostic cache was a little bit slower than the GC-specific cache, in terms of AOT cache size, these two implementations are almost the same:
- GC-specific - 123813888 bytes
- GC-agnostic - 124047360 bytes
As always, I encourage you to perform your own measurements. Performance characteristics can vary significantly depending on your specific application.
Conclusion
The upcoming release of JDK 26 brings another useful feature to Project Leyden. By making the AOT cache compatible with ZGC, the project is becoming much more flexible for different use cases.
It is great to see Project Leyden moving forward and delivering new JEPs with every JDK release.
Reviewed by Szymon Winiarz
