Final

JEP 516: Having Your Cake (Fast Startup) and Eating It Too (Low Latency)

I have become a greater fan of Project Leyden with every deliverable it produced. I admit, I was sceptical when Chief Language Architect Brian Goetz announced Project Leyden. It felt as if some answer was needed after the arrival of GraalVM and its native images, that had a blistering startup speed. But how could we ever increase the startup of the JVM? Turns out, there are quite of few possibilities.

JEP 483, Ahead-of-Time classloading & linking was the foundational JEP that lay much of the groundwork that was required. It enables the JVM to take a snapshot of loaded and linked classes during a so-called training run and store this information in a cache. This cache is then used when the application is ran in production, allowing it to skip the class loading and linking part, but using the information in the cache instead. It significantly improves start-up times. Later JEPs introduced simplified launch methods and the possibility to preserve method execution profiles across runs, allowing the JVM to “learn” the application behavior to improve startup performance.

If you’ve been following Project Leyden’s recent wins for Java startup time, like me, you might have hit a frustrating wall: you had to choose between fast startup (using the AOT cache) and low latency (using ZGC). You couldn’t have both.

JEP 516 fixes that in Java 26.

The Problem: The “Closed Garden” of Caching Previously, the Ahead-of-Time (AOT) cache introduced in JDK 24 was essentially taking a snapshot of the Java heap and saving it to disk. This snapshot was “GC-specific,” meaning it hardcoded memory addresses and object headers exactly as a specific Garbage Collector (like G1) expected them.

This was a dealbreaker for ZGC (the Z Garbage Collector). ZGC uses “colored pointers”—it stores metadata bits directly inside object references—so it couldn’t read a cache generated by a different GC, nor could its own complex pointer format be easily cached using the old method. This forced developers to choose: do you want the instant startup of Leyden or the sub-millisecond pauses of ZGC?.

The Solution: Streaming Objects JEP 516 introduces a “GC-agnostic” cache format. Instead of saving raw memory addresses (which tie you to a specific GC configuration), the cache saves objects using “logical indices”.

Think of it as the difference between mailing someone a pre-assembled piece of furniture (the old way—fast to use, but hard to ship if the door is too small) vs. mailing them the instructions and parts (the new way—fits anywhere, assembled on arrival).

When you start your app:

  1. The JVM streams objects from the cache into the heap sequentially.
  2. A background thread “materializes” them, converting those logical indices into actual memory addresses compatible with whatever GC you are running—including ZGC.

Why it’s cool:

  • ZGC Compatibility: You can finally combine the massive startup boost of AOT caching with the low-latency guarantees of ZGC.
  • Smart Heuristics: The JVM is smart enough to choose the best format automatically. If you are running a massive heap (over 32GB) or using ZGC during training, it defaults to this flexible “streaming” format. If you are in a constrained environment with a small heap, it might stick to the old “mapping” style for raw speed.

In short: You no longer have to compromise on garbage collection just to get your app to start fast. Java 26 lets you use the best GC for the job without sacrificing startup performance.