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 String instances (huge in frameworks with lots of annotations and logging)
  • Class mirrors (Class<?> objects for loaded classes)
  • Objects eagerly initialized during startup (Spring’s ApplicationContext components, CDI beans, etc.)
  • Reflection metadata (Method, Field, Constructor objects)

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.Main

This 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.Main

The 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.Main

Expected 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 JVMjshell, 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.jar

Tip: Regenerate the .aot cache 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.