These logs show Generational ZGC in action, handling memory pressure with frequent young generation collections. Here’s what’s happening:
Trigger (Young): Learning ... indicates that ZGC is in its learning phase, adjusting internal heuristics and thresholds based on observed behavior. The GC is triggered because free memory dropped below a threshold (e.g., 357M < 358M).
GC(0) Concurrent reset (Young) begins the collection by resetting internal GC state. This phase runs concurrently, meaning the application keeps running.
Pause Init Mark (Young) is a short stop-the-world phase to mark GC roots.
The following stages (remembered set scanning, marking roots, marking, weak references, etc.) all run concurrently, making ZGC very low-pause.
Pause Final Mark (Young) is another brief pause to finish marking before cleanup.
Concurrent cleanup (Young) 135M->109M(492M) shows how much memory was reclaimed (from 135MB down to 109MB out of 492MB).
Pause Final Roots (Young) is the final short pause before GC finishes.
📌 The key idea: Generational ZGC maintains very short pause times by doing most of its work concurrently — including root scanning and cleanup — while focusing primarily on the young generation, just like Gen Shenandoah or G1.
The longLived object is allocated in the Young Generation, but since it remains referenced across many GC cycles, it is eventually promoted to the Old Generation by Generational ZGC. While ZGC logs don’t explicitly label promotions, you can infer them when memory usage in the Young Gen stays high between collections. For deeper analysis, use JFR or enable verbose diagnostics.
Comparison with G1:
chmod +x run-gc.sh
mvn compile
./scripts/jep404/run-gc.sh # Defaults to Generational Shenandoah
./scripts/jep404/run-gc.sh gen # Explicit Generational Shenandoah
./scripts/jep404/run-gc.sh g1 # G1 Garbage Collector
./scripts/jep404/run-gc.sh zgc # Z Garbage Collector
The logs show how G1 (Garbage-First) GC handles memory under pressure from frequent large allocations (in this case, "humongous" objects):
Pause Young (Concurrent Start) means G1 is collecting the young generation and starting a concurrent phase afterward.
(G1 Humongous Allocation) indicates the allocation triggered a collection because the object was too large to fit in a regular region (G1 treats any object ≥ 50% of region size as humongous).
222M->102M(496M) shows heap usage before → after GC, with total heap capacity.
Concurrent Undo Cycle is G1’s mechanism to roll back partially completed concurrent phases, likely because another GC had to interrupt or overlap.
The logs reflect a pattern of rapid Young GCs due to aggressive allocation, which is typical for G1 when large short-lived objects are created in tight loops.
Standard non-generational ZGC
[0.230s][info][gc] GC(0) Major Collection (Warmup)
[0.242s][info][gc] GC(0) Major Collection (Warmup) 106M(21%)->108M(21%) 0.012s
[0.250s][info][gc] GC(1) Major Collection (Warmup)
[0.259s][info][gc] GC(1) Major Collection (Warmup) 108M(21%)->108M(21%) 0.009s
Performing Allocation
[0.280s][info][gc] GC(2) Major Collection (Warmup)
[0.299s][info][gc] GC(2) Major Collection (Warmup) 158M(31%)->206M(40%) 0.019s
[0.340s][info][gc] GC(3) Minor Collection (Allocation Rate)
[0.344s][info][gc] GC(3) Minor Collection (Allocation Rate) 334M(65%)->142M(28%) 0.004s
[0.367s][info][gc] GC(4) Major Collection (Allocation Rate)
[0.385s][info][gc] GC(4) Major Collection (Allocation Rate) 350M(68%)->252M(49%) 0.018s
[0.392s][info][gc] GC(5) Minor Collection (Allocation Rate)
[0.396s][info][gc] GC(5) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s
[0.416s][info][gc] GC(6) Minor Collection (Allocation Rate)
[0.420s][info][gc] GC(6) Minor Collection (Allocation Rate) 380M(74%)->140M(27%) 0.003s
[0.437s][info][gc] GC(7) Minor Collection (Allocation Rate)
[0.441s][info][gc] GC(7) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s
[0.451s][info][gc] GC(8) Minor Collection (Allocation Rate)
[0.456s][info][gc] GC(8) Minor Collection (Allocation Rate) 300M(59%)->172M(34%) 0.004s
[0.469s][info][gc] GC(9) Minor Collection (Allocation Rate)
[0.472s][info][gc] GC(9) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s
[0.485s][info][gc] GC(10) Minor Collection (Allocation Rate)
[0.489s][info][gc] GC(10) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s
non-generational ZGC treats the entire heap as a single space (no young/old gen separation)
Key GC Events in the Log:
🔸 Warmup GCs:
scssCopyEdit[0.230s][info][gc] GC(0) Major Collection (Warmup)
[0.242s][info][gc] GC(0) Major Collection (Warmup) 106M(21%)->108M(21%) 0.012s
ZGC warms up its GC cycles to calibrate thresholds and prepare for regular allocations.
These are full-heap collections (because ZGC has no generational layout yet).
Minor memory changes here indicate low allocation pressure during warmup.
🔸 Major Collection (Allocation Rate):
scssCopyEdit[0.367s][info][gc] GC(4) Major Collection (Allocation Rate)
[0.385s][info][gc] GC(4) Major Collection (Allocation Rate) 350M->252M 0.018s
A major collection reclaims memory across the whole heap.
Triggered when allocation rate increases and ZGC decides a full collection is needed.
🔸 Minor Collections:
scssCopyEdit[0.340s][info][gc] GC(3) Minor Collection (Allocation Rate)
[0.344s][info][gc] GC(3) Minor Collection (Allocation Rate) 334M->142M 0.004s
Minor collections still happen in non-generational ZGC but aren’t targeting a young gen (yet).
These are shorter and more frequent when short-lived objects dominate.
Why non-generational ZGC is faster for simple example?
TL;DR;
Standard ZGC shows very short GC pause times in this example (e.g., 0.004s) because it avoids the overhead of generational tracking.
While Generational ZGC introduces a Young/Old generation split (like G1 or Shenandoah), it adds bookkeeping costs that make its pause times slightly longer in synthetic allocation-heavy workloads.
In real-world apps with longer-lived objects, Generational ZGC scales better by avoiding full-heap collection and promoting surviving objects.
Longer:
ven though Generational ZGC was added in Java 21+ (and improved in 22–24), non-generational ZGC can sometimes show lower pause times due to:
1. 🧠 Simpler collection process
In non-generational ZGC, there’s no remembered set or generational promotion tracking.
Less internal bookkeeping = faster processing per GC cycle.
2. ⚖️ Smaller per-cycle workload
Each collection may reclaim fewer objects but also does less work.
Minor GC in non-gen ZGC is less accurate but cheaper than Gen ZGC's more structured young collection.
3. 🚀 Concurrent design
ZGC is designed to minimize pause times (target: <1ms).
Most of the GC work (mark, relocate, remap) is fully concurrent in both modes.
Differences in implementation overhead can lead to non-generational mode being slightly faster in synthetic tests.
4. 🧪 Your test is allocation-heavy but not aging objects
In your test, objects don’t survive long enough to promote.
So Generational ZGC’s "promotion + remembered set" logic doesn’t get much benefit, but still incurs its cost.