ZGC and Shenandoah: Low-Pause GCs in Java 15
Introduction
A garbage collector (GC) frees the memory of objects a program no longer uses. Most collectors must sometimes stop all of the application's threads to do part of their work - a pause. With large heaps, these pauses could last hundreds of milliseconds or even seconds, which is a problem for services that must answer quickly. Java 15Click to read tutorial: Overview of New Features in Java 15 made two low-pause garbage collectors production-ready: ZGC (JEP 377), developed by Oracle, and Shenandoah (JEP 379), developed by Red Hat. Both do almost all their work while the application keeps running, so pauses stay at around a millisecond or less, whatever the size of the heap. This tutorial explains how they achieve this, how they compare with the default G1 collector, and how to enable and observe them, with a runnable comparison.
Why garbage collectors pause
To free memory, a collector must find the live objects (marking), and then usually move them together so that free memory is in one large block (compaction). Moving an object is the hard part: every reference to it, anywhere in the heap or on any thread's stack, must be updated to point to the new address. The simplest way to do that safely is to stop all application threads while objects are moved. G1, the default collector since Java 9, marks the old generation concurrently, but still moves objects in stop-the-world pauses, whose length grows with the amount of live data being moved.
How ZGC and Shenandoah avoid long pauses
Both collectors move objects concurrently, while application threads are running. To make that safe, they intercept the application's accesses to object references with small pieces of code called barriers -
Enabling the collectors
In Java 15 neither collector needs
Note - Shenandoah is included in most OpenJDK builds, such as Eclipse Temurin, Amazon Corretto and Red Hat's builds, but not in Oracle's JDK builds, where
Running it with each collector and a 256 MB heap -
OUTPUT with G1 (the default)
OUTPUT with ZGC
OUTPUT with Shenandoah
Explanation of the output
Which collector should you use?
- ZGC uses load barriers and colored pointers: a few bits of each 64-bit reference record the state of the object it points to. When the application loads a reference which points to an object that has been moved, the barrier notices from the color bits and fixes the reference on the spot.
- Shenandoah also uses load barriers to make sure the application always sees the moved copy of an object, and copies objects concurrently with the application.
-XX:+UnlockExperimentalVMOptions any more -
java -XX:+UseZGC -Xmx4g -jar app.jar java -XX:+UseShenandoahGC -Xmx4g -jar app.jar
-XX:+UseShenandoahGC fails with "Option -XX:+UseShenandoahGC not supported".
Comparing G1, ZGC and Shenandoah
The program below allocates objects as fast as it can for two seconds, keeping some of them alive, and then reports what each garbage collector did, using the JVM's GarbageCollectorMXBeans -
Measuring collections and pauses
//GcComparison.java
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class GcComparison {
public static void main(String[] args) {
List<byte[]> live = new ArrayList<>();
Random random = new Random(1);
long allocated = 0, end = System.nanoTime() + 2_000_000_000L;
while (System.nanoTime() < end) { // allocate for 2 seconds
byte[] block = new byte[1024 + random.nextInt(8192)];
allocated += block.length;
if (random.nextInt(100) < 3) { // keep about 3% alive for a while
live.add(block);
if (live.size() > 20_000) live.subList(0, 10_000).clear();
}
}
System.out.printf("Allocated about %,d MB in 2 seconds%n", allocated / (1024 * 1024));
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.printf(" %-24s count=%-5d time=%d ms%n", gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
}
}
}
java -Xmx256m GcComparison java -Xmx256m -XX:+UseZGC GcComparison java -Xmx256m -XX:+UseShenandoahGC GcComparison
Allocated about 27,197 MB in 2 seconds G1 Young Generation count=218 time=324 ms G1 Concurrent GC count=92 time=14 ms G1 Old Generation count=0 time=0 ms
Allocated about 23,471 MB in 2 seconds ZGC Minor Cycles count=333 time=700 ms ZGC Minor Pauses count=1027 time=6 ms ZGC Major Cycles count=47 time=388 ms ZGC Major Pauses count=207 time=1 ms
Allocated about 14,851 MB in 2 seconds Shenandoah Pauses count=469 time=30 ms Shenandoah Cycles count=117 time=435 ms
- These numbers are from one run on a Windows laptop with a recent OpenJDK build (Eclipse Temurin 25), and will differ on your machine and from run to run - but the pattern is typical.
- For G1, the "Young Generation" time is time spent in stop-the-world pauses. For ZGC and Shenandoah the JVM reports Pauses and Cycles separately: the cycles are the concurrent work, done while the application ran, and only the pause time actually stopped the application. Compare the total pause times: ZGC and Shenandoah stopped the application for a small fraction of G1's pause time, with each pause far shorter on average.
- The amount allocated shows the other side of the trade-off: in this run G1 got through the most allocation. Low-pause collectors spend CPU on concurrent work and barriers, which can reduce throughput.
- ZGC's output shows Minor and Major collections because, since Java 21, ZGC is generational - it collects young objects more often than old ones, which made it more efficient. In Java 15, ZGC was not yet generational.
- G1 (default) - a good balance of throughput and pause times for most applications. Start here.
- ZGC or Shenandoah - for latency-sensitive services, such as APIs with strict response-time targets, trading systems or interactive applications, and especially with large heaps, where G1's pauses would grow.
- Parallel GC (
-XX:+UseParallelGC) - for batch jobs where only total throughput matters and pauses are acceptable.
-Xlog:gc) or the Java Flight RecorderClick to read tutorial: Java 14 JFR Event Streaming Tutorial with Examples, before and after switching.
What happened next
Both collectors kept improving after Java 15. ZGC became generational in Java 21 (read about generational ZGCClick to read: read about generational ZGC), generational mode became the default in Java 23, and the old non-generational mode was removed in Java 24. Shenandoah gained an experimental generational mode in Java 24, which became a product feature in Java 25.
Summary
In this tutorial we saw why garbage collectors pause - moving objects requires every reference to them to be updated - and how ZGC and Shenandoah, production-ready since Java 15, move objects concurrently using load barriers, keeping pauses short regardless of heap size. We enabled them with -XX:+UseZGC and -XX:+UseShenandoahGC, compared them with G1 on the same workload, and saw the trade-off between pause times and throughput. Lastly, we looked at how to choose a collector, and how both collectors evolved later. Next, read about hidden classesClick to read tutorial: Java 15 Hidden Classes Explained with Examples.Continue Reading Java 12 to 17 Tutorials
Overview of New Features in Java 15Click to read tutorial: Overview of New Features in Java 15 Java Text Blocks: Multi-line Strings with ExamplesClick to read tutorial: Java Text Blocks: Multi-line Strings with Examples Java 15 String Methods: formatted, stripIndent and MoreClick to read tutorial: Java 15 String Methods: formatted, stripIndent and More Java 15 Hidden Classes Explained with ExamplesClick to read tutorial: Java 15 Hidden Classes Explained with Examples Overview of New Features in Java 16Click to read tutorial: Overview of New Features in Java 16 Overview of New Features in Java 12Click to read tutorial: Overview of New Features in Java 12 Overview of New Features in Java 13Click to read tutorial: Overview of New Features in Java 13 Overview of New Features in Java 14Click to read tutorial: Overview of New Features in Java 14 Overview of New Features in Java 17Click to read tutorial: Overview of New Features in Java 17
Overview of New Features in Java 15Click to read tutorial: Overview of New Features in Java 15 Java Text Blocks: Multi-line Strings with ExamplesClick to read tutorial: Java Text Blocks: Multi-line Strings with Examples Java 15 String Methods: formatted, stripIndent and MoreClick to read tutorial: Java 15 String Methods: formatted, stripIndent and More Java 15 Hidden Classes Explained with ExamplesClick to read tutorial: Java 15 Hidden Classes Explained with Examples Overview of New Features in Java 16Click to read tutorial: Overview of New Features in Java 16 Overview of New Features in Java 12Click to read tutorial: Overview of New Features in Java 12 Overview of New Features in Java 13Click to read tutorial: Overview of New Features in Java 13 Overview of New Features in Java 14Click to read tutorial: Overview of New Features in Java 14 Overview of New Features in Java 17Click to read tutorial: Overview of New Features in Java 17