Skip to content
Back
JVM internals

ZGC and Shenandoah: Low-Latency Garbage Collection

Sub-millisecond pauses, concurrent compaction, generational modes in Java 25, and Epsilon, the collector that collects nothing.

Michał Artur Marciniak

3 min read

On this page
  1. ZGC - The Time Lord
  2. Shenandoah - The Memory Whisperer
  3. The Epsilon Collector - The Sound of Silence
  4. Java 25 Innovations - The New Frontier

When pause times matter more than raw throughput, the JVM offers two concurrent collectors that keep pauses below a millisecond, plus Epsilon, which skips collection entirely. This part also covers what Java 25 changed.

ZGC - The Time Lord

Pauses
Under 10 ms at any heap size
Throughput
At most 15% overhead
Heap
Up to 16 TB
GC threads
Many, almost all concurrent

Ultra-Low Latency at Scale

Z Garbage Collector (ZGC) is Oracle’s answer to the latency problem. Design goals:

  • Pause times under 10ms regardless of heap size
  • Scalability to 16TB heaps
  • No more than 15% throughput overhead

ZGC achieves this through a combination of techniques:

  1. Concurrent operations: Marking, relocation, reference processing - all happen while application threads run
  2. Colored pointers: Pointer metadata tracks object state (marked, relocated, etc.) without touching the object itself
  3. Load barriers: Every object read checks if relocation is needed, enabling concurrent compaction
  4. Generational by default (Java 23+): Separates young and old collections for better efficiency - now the standard mode

The ZGC Architecture

The slow path of the ZGC load barrierSequence with 3 participants and 6 messagesApplication threadApplication threadLoad barrierLoad barrierGC relocationGC relocationReads a referenceChecks the colourObject moved?New addressHeals the pointerCorrect reference
  1. Application thread → Load barrierReads a reference
  2. Load barrierChecks the colour
  3. Load barrier → GC relocationObject moved?
  4. GC relocation → Load barrierNew address
  5. Load barrierHeals the pointer
  6. Load barrier → Application threadCorrect reference
The slow path of the load barrier. When the colour of the pointer is right, the barrier returns the reference at once. When the object has moved, it fetches the new address and heals the pointer in memory, so the next read takes the fast path.

Enabling ZGC

Shell
# Java 15-21 (non-generational)
java -XX:+UseZGC -Xms16g -Xmx16g MyApp

# Java 21-22 (generational ZGC - explicit flag required)
java -XX:+UseZGC -XX:+ZGenerational -Xms16g -Xmx16g MyApp

# Java 23+ (generational by default)
java -XX:+UseZGC -Xms16g -Xmx16g MyApp

# Java 24+ (non-generational mode removed - JEP 490)
java -XX:+UseZGC -Xms16g -Xmx16g MyApp

Note: ZGC is generational by default since Java 23. The -XX:+ZGenerational flag was deprecated in Java 23 and non-generational mode was completely removed in Java 24 (JEP 490). In Java 25, generational ZGC is mature and production-hardened.

Note: On Java 25, ZGC works with Compact Object Headers (JEP 519, -XX:+UseCompactObjectHeaders), so the two can be combined.

When to Choose ZGC

  • Sub-10ms p99 latency: consistently delivers
  • Large heaps (16GB+): pause time independent of heap size
  • Cloud/container environments: efficient memory return to OS
  • Latency-sensitive services: no tuning typically needed

The Trade-Off

ZGC uses more CPU and memory than G1 or Parallel. It reserves address space aggressively and has higher memory overhead. But for latency-critical applications, this is a fair exchange.

Container Tip: ZGC works well in containers but set -XX:MaxRAMPercentage=75.0 to leave headroom for GC overhead. Don’t allocate 100% of container memory to the heap.


Shenandoah - The Memory Whisperer

Pauses
Under 10 ms at any heap size
Throughput
Slightly higher than ZGC
Heap
Up to 16 TB
GC threads
Many, evacuation concurrent

Concurrent Compaction Pioneer

Developed by Red Hat and merged into OpenJDK, Shenandoah shares ZGC’s goals but uses different techniques:

  • Concurrent evacuation: Objects are moved while application threads run
  • Forwarding pointers: The old copy of an evacuated object points to the new one. Shenandoah started with a separate Brooks pointer in every object; since JDK 13 the forwarding pointer lives in the header of the old copy instead
  • Load reference barriers: Every reference loaded from the heap resolves to the current copy of the object. They replaced the earlier read and write barriers in JDK 13
  • Pause times independent of heap size - a 200GB heap collects as fast as a 2GB heap

Shenandoah’s Phases

Shenandoahone cycle
MarkingEvacuationUpdate references
Applicationruns, except for three pauses
  1. Initial mark pause
  2. Marking
  3. Final mark pause
  4. Evacuation
  5. Update references
  6. Final update refs pause
The violet lines are the three short stop-the-world pauses, each under 1 ms. The grey phases run concurrently with the application. An illustration, not to scale.

Enabling Shenandoah

Shell
# Available in OpenJDK 12+, Red Hat/OpenLogic builds
java -XX:+UseShenandoahGC -Xms8g -Xmx8g MyApp

# Heuristics: when Shenandoah starts a cycle
java -XX:ShenandoahGCHeuristics=adaptive ...  # Default: from the timing of previous cycles
java -XX:ShenandoahGCHeuristics=static ...    # From heap occupancy
java -XX:ShenandoahGCHeuristics=compact ...   # Back to back: smallest heap, lower throughput

ZGC vs Shenandoah

ZGCShenandoah
AvailabilityOracle JDK, OpenJDKOpenJDK, Red Hat builds
GenerationalDefault since Java 23, the only mode since Java 24Since Java 25
Memory overheadHigherLower than ZGC
ThroughputSlightly lowerSlightly higher
Shared: heaps up to 16 TB and pauses under 10 ms. These are the only differences.

Both are excellent choices. In 2026, both ZGC and Shenandoah have generational modes. ZGC’s generational implementation has been default since Java 23, while Shenandoah added generational support in Java 25 (JEP 521). For ultra-low latency workloads, both collectors now offer mature, production-ready generational collection.


The Epsilon Collector - The Sound of Silence

Pauses
None, it never collects
Throughput
No GC overhead
Heap
Until it fills, then the JVM exits
GC threads
None

No-Op for Performance Testing

Epsilon is the anti-collector - it allocates memory but never collects. When the heap fills up, the JVM exits.

Use it for:

  • Performance testing (eliminate GC as a variable)
  • Short-lived applications (one-shot tools)
  • Determining if you need a collector at all (if Epsilon works, you have no GC pressure)
Shell
java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC MyApp

Warning: Epsilon is not for production. Your application will crash when memory fills. This is by design.


Java 25 Innovations - The New Frontier

Generational Shenandoah (JEP 521)

While ZGC went generational by default in Java 23, Java 25 brought the same treatment to Shenandoah (JEP 521). This is significant because:

  • Shenandoah has lower memory overhead than ZGC
  • Both collectors now offer generational modes
  • Applications with typical object lifetime patterns see reduced pause times

Enable it (Java 25+):

Shell
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xms8g -Xmx8g MyApp

The Java 25 LTS Recommendation

For production deployments in 2026, Java 25 LTS (released September 2025) should be your target:

Shell
# Recommended Java 25 configuration for low-latency services
java \
  -XX:+UseZGC \
  -XX:+AlwaysPreTouch \
  -Xms4g \
  -Xmx4g \
  -jar application.jar

Deep dive: For detailed coverage of Java 25’s performance improvements including Project Leyden’s AOT profiling and Scoped Values, see JVM: The Silent Revolution.