Java 21 Generational ZGC Explained

Introduction ZGC, production-ready since Java 15Click to read: production-ready since Java 15, keeps garbage collection pauses below a millisecond by doing nearly all its work while the application runs. But the original ZGC had a weakness: it treated every object the same, so each collection had to mark through the whole heap, including long-lived objects which rarely change. With high allocation rates it needed a lot of spare memory and CPU to keep up. Java 21Click to read tutorial: Overview of New Features in Java 21 (JEP 439) made ZGC generational. This tutorial explains generational ZGC: the generational hypothesis behind it, how it works, how to enable it, how it evolved after Java 21, and how it behaves on a runnable example. The generational hypothesis Most objects die young. A request handler allocates strings, lists and temporary objects which are garbage moments later, while caches, connection pools and configuration live for the whole run. This observation, the weak generational hypothesis, is why most collectors - including G1, Parallel and Serial - split the heap into a young generation for new objects and an old generation for objects that survive. The young generation is collected often and cheaply, since it is mostly garbage; the old generation is collected rarely. Non-generational ZGC gave up this advantage in exchange for simplicity.
How generational ZGC works
  • The heap is divided into a young and an old generation. New objects are allocated in the young generation, and objects which survive several young collections are promoted to the old one.
  • Minor collections collect only the young generation, frequently. Major collections collect both generations, much less often.
  • Both kinds of collection are still concurrent: marking, moving and reference fixing run alongside the application, using load barriers and colored pointers, plus new store barriers which track references from old objects to young ones. Pauses stay well below a millisecond.
The result, according to the JEP's benchmarks, is that generational ZGC needs less heap and less CPU than non-generational ZGC for the same workload, and keeps up with higher allocation rates, without giving up its low pause times. Enabling it, release by release
  • Java 21: generational mode was opt-in: -XX:+UseZGC -XX:+ZGenerational. Plain -XX:+UseZGC still gave the old, non-generational ZGC.
  • Java 23 (JEP 474): generational mode became the default for -XX:+UseZGC, and the non-generational mode was deprecated.
  • Java 24 (JEP 490): the non-generational mode was removed, so ZGC is always generational and the ZGenerational option is obsolete.
On a current JDK, then, -XX:+UseZGC is all you need, and asking for the old mode only produces a warning -
java -XX:+UseZGC -XX:-ZGenerational -version
 OUTPUT of the above command
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option ZGenerational; support was removed in 24.0
java version "24.0.1" 2025-04-15
Java(TM) SE Runtime Environment (build 24.0.1+9-30)
Java HotSpot(TM) 64-Bit Server VM (build 24.0.1+9-30, mixed mode, sharing)
Watching minor and major collections The program below creates many short-lived objects, as a server would, while keeping a long-lived cache in memory - a workload which suits a generational collector - and then reports what ZGC did -
A workload with young and old objects
//GenerationalZgcDemo.java
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GenerationalZgcDemo {
  public static void main(String[] args) {
    Map<Integer, String> cache = new HashMap<>();             // long-lived objects
    for (int i = 0; i < 200_000; i++) cache.put(i, "value-" + i);

    long requests = 0, end = System.nanoTime() + 2_000_000_000L;
    while (System.nanoTime() < end) {                         // short-lived objects
      List<String> temp = new ArrayList<>();
      for (int i = 0; i < 50; i++) temp.add("request-" + requests + "-" + i);
      requests++;
    }
    System.out.printf("Handled %,d requests with a cache of %,d entries%n", requests, cache.size());
    for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
      System.out.printf("  %-18s count=%-5d time=%d ms%n", gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
    }
  }
}
java -XX:+UseZGC -Xmx512m GenerationalZgcDemo
 OUTPUT of the above command
Handled 1,398,222 requests with a cache of 200,000 entries
  ZGC Minor Cycles   count=19    time=51 ms
  ZGC Minor Pauses   count=57    time=1 ms
  ZGC Major Cycles   count=3     time=312 ms
  ZGC Major Pauses   count=15    time=0 ms
Explanation of the output
  • These numbers are from one run on a laptop with a recent JDK, and will vary, but the pattern is typical: there are many more minor cycles than major ones, because the young generation fills up quickly with dead request objects while the cache sits in the old generation.
  • The "Cycles" times are concurrent work, done while the application kept running; the "Pauses" are the only time the application was actually stopped, and they add up to very little.
  • To see each collection as it happens, add -Xlog:gc, which logs lines such as GC(3) Minor Collection (Allocation Rate) with their timings.
ZGC or G1? G1 remains the default collector and a good choice for most applications. Generational ZGC is the better choice when consistently low latency matters - interactive services with strict response-time targets - and especially with large heaps, where G1's pauses would grow. As always, measure with your own workload, for example with GC logs or Java Flight RecorderClick to read: Java Flight Recorder. Summary In this tutorial we saw how Java 21 made ZGC generational, applying the weak generational hypothesis - most objects die young - to a concurrent, low-pause collector. We went through how generational ZGC collects the young generation often in minor collections and the whole heap rarely in major collections, how it was opt-in in Java 21, the default in Java 23 and the only mode from Java 24, and watched minor and major collections on a workload with short-lived and long-lived objects. Next, read about the Java 21 API additionsClick to read tutorial: Java 21 API Additions: StringBuilder.repeat, Math.clamp.