Java 26: Ahead-of-Time Object Caching with Any GC (JEP 516)
Faster startup and lower initial GC pressure — without changing a line of application code. JEP 516 extends Java’s Ahead-of-Time (AOT) cache to include heap objects, and lifts the restriction that previously limited this to SerialGC only. In JDK 26, every garbage collector can benefit.
Status: Standard — controlled entirely via JVM flags, no code changes required.
Background: The AOT Cache Journey
| JDK | Milestone |
|---|---|
| JDK 5 (2004) | Class Data Sharing (CDS) — cache loaded class metadata |
| JDK 10 (2018) | Application CDS (JEP 310) — extend CDS to application classes |
| JDK 24 (2025) | AOT Class Loading & Linking (JEP 483) — cache class loading + linking work |
| JDK 25 (2025) | AOT Method Compilation (JEP 485) — cache JIT-compiled methods |
| JDK 26 (2026) | AOT Object Caching (JEP 516) — cache heap objects, any GC |
What JEP 516 Adds
Previous AOT caching (JEP 483) stored class metadata and bytecode in a pre-built file on disk. The JVM could reload this on startup instead of re-parsing class files.
JEP 516 goes further: it also serializes live Java heap objects into the AOT cache. On the next run, those objects are mapped directly into the heap — no allocation, no initialization code to run.
What kinds of objects are cached?
- Interned
Stringinstances (huge in frameworks with lots of annotations and logging) Classmirrors (Class<?>objects for loaded classes)- Objects eagerly initialized during startup (Spring’s
ApplicationContextcomponents, CDI beans, etc.) - Reflection metadata (
Method,Field,Constructorobjects)
Before JDK 26: AOT object caching required -XX:+UseSerialGC.
JDK 26: Works with G1GC (default), ZGC, Shenandoah, ParallelGC, SerialGC.
How to Use It
AOT caching follows a two-step workflow: record then replay.
Step 1: Record the AOT Cache
Run your application once with -XX:AOTMode=record. The JVM profiles the startup, records class loading and object allocation, and writes the cache file:
java -XX:AOTMode=record \
-XX:AOTCache=myapp.aot \
-cp myapp.jar com.example.MainThis run behaves normally — the application starts and (ideally) performs a complete startup sequence so the JVM can observe all the objects worth caching.
Step 2: Run with the AOT Cache
On all subsequent runs, point to the cache file. The JVM maps the cached objects directly into memory instead of re-initializing them:
java -XX:AOTMode=on \
-XX:AOTCache=myapp.aot \
-cp myapp.jar com.example.MainThe startup objects are present in the heap before main() even runs.
Optional: Verify Cache Usage
Use -Xlog to see which classes and objects were loaded from the cache:
java -XX:AOTMode=on \
-XX:AOTCache=myapp.aot \
-Xlog:aot=info \
-cp myapp.jar com.example.MainExpected Benefits
| Metric | Typical Improvement |
|---|---|
| Startup time | 10%–40% reduction (varies by framework) |
| Time-to-first-request | Significant reduction for HTTP servers |
| Initial GC pressure | Reduced — fewer allocations at startup means fewer minor collections |
| Memory at steady state | Largely unchanged — cached objects still count toward heap |
Benefits are most pronounced for:
- Framework-heavy applications (Spring Boot, Quarkus, Micronaut) — lots of reflection, annotation processing, and bean initialization
- Microservices with fast restarts — Kubernetes rolling updates, serverless functions
- CLI tools built on JVM —
jshell, build tools, custom tooling
GC-Specific Notes
With JEP 516, the AOT cache can be used with any GC — but the mechanics differ slightly per collector:
| GC | AOT Object Caching | Notes |
|---|---|---|
| G1GC (default) | ✅ JDK 26 | Cached objects placed in old-gen regions |
| ZGC | ✅ JDK 26 | Works with ZGC’s colored pointers |
| Shenandoah | ✅ JDK 26 | Works with Shenandoah’s forwarding pointers |
| ParallelGC | ✅ JDK 26 | Standard old-gen placement |
| SerialGC | ✅ JDK 24+ | First collector to support AOT objects |
Practical Workflow for Spring Boot
# 1. Build your fat JAR
mvn package -DskipTests
# 2. Record the AOT cache (do a full startup + shutdown)
java -XX:AOTMode=record \
-XX:AOTCache=myapp.aot \
-jar target/myapp.jar \
--spring.main.web-application-type=none # optional: skip HTTP listener for recording
# 3. Run in production with the cache
java -XX:AOTMode=on \
-XX:AOTCache=myapp.aot \
-jar target/myapp.jarTip: Regenerate the
.aotcache after any JAR change. The JVM validates the cache fingerprint against the classpath — a mismatched cache is silently ignored, not an error.
Relationship to GraalVM Native Image
AOT object caching is not Native Image. The application still runs on the HotSpot JVM, gets JIT-compiled code, and can use the full JDK API. Think of it as “turbo-charging the JVM startup path” rather than compiling to a native binary:
| AOT Cache (JEP 516) | GraalVM Native Image | |
|---|---|---|
| Execution model | HotSpot JVM + JIT | Native binary, no JVM |
| Full JDK API | ✅ | ⚠️ Partial (closed-world) |
| Peak throughput | ✅ JIT-optimized | ⚠️ Usually lower |
| Startup improvement | Moderate (10%–40%) | Very large (10×+) |
| Build complexity | Low (two CLI commands) | High (closed-world analysis) |
| Code changes required | None | Sometimes |
AOT caching and Native Image are complementary strategies. Many teams use AOT caching for the JVM path and Native Image for latency-critical edge deployments.
Checking Cache Effectiveness
After enabling the AOT cache, verify it is working:
# Add to JVM flags to print AOT statistics at shutdown
-Xlog:aot+statistics=info
# Sample output:
# [info][aot,statistics] AOT cache: 12,847 classes loaded from cache
# [info][aot,statistics] AOT cache: 84,231 objects restored from cache
# [info][aot,statistics] AOT cache: startup time reduced by 38%If the cache is ignored (e.g., classpath changed), you’ll see:
[warning][aot] AOT cache myapp.aot is invalid — ignoring (classpath mismatch)No crash, no error — the JVM simply falls back to normal startup.