Azul Statement on the eMudhra Intermediate CA Certificates Compromise
high-performance-jvm-bg

Generational ZGC vs G1GC

Smart Summary

Generational ZGC in Java 21 narrows the throughput gap with G1GC while retaining sub-millisecond pauses. This guide compares the two collectors on latency and memory trade-offs and explains when Azul Prime’s C4 takes latency further.

Introduction

Generational ZGC, introduced in Java 21, changes the calculus of the ZGC vs G1GC debate. Prior to Java 21, choosing ZGC over G1GC meant accepting a meaningful throughput penalty — often 10-15% — in exchange for sub-millisecond pause times. Generational ZGC applies the decades-proven generational hypothesis to ZGC’s concurrent framework, dramatically narrowing that gap.

For Java 21 adopters — a natural inflection point given its LTS status — this creates a genuine re-evaluation: does Generational ZGC now offer enough throughput improvement to make it the better default for latency-sensitive workloads? Or is G1GC still the pragmatic choice for most applications? This comparison examines the technical trade-offs in depth, with specific attention to Java 21-era changes.

What Is Generational ZGC?

Generational ZGC is an evolution of ZGC that introduces a young/old generation split to the ZGC architecture. Non-generational ZGC treats all heap objects uniformly, applying its concurrent collection algorithm across the entire heap. Generational ZGC recognizes that most objects die young — the generational hypothesis — and focuses collection effort on the young generation, where garbage density is highest.

To enable Generational ZGC in Java 21, use: -XX:+UseZGC -XX:+ZGenerational. In Java 23 and later, generational mode is the default when ZGC is selected; the flag is no longer needed.

The key improvement is throughput: by spending more time collecting the young generation (where most allocation occurs) and less time scanning the full heap, Generational ZGC reduces total GC overhead. This translates to higher application throughput compared to non-generational ZGC, often narrowing the gap with G1GC to 5% or less on allocation-heavy workloads.

Critically, Generational ZGC retains ZGC’s fundamental latency properties. STW pause times remain under 1ms and remain nearly independent of heap size. The generational structure improves throughput without sacrificing the low-latency characteristics that make ZGC compelling.

Key Generational ZGC characteristics:

  • Young/old generation split applied to ZGC’s concurrent framework
  • STW pauses: under 1ms, heap-size-independent (unchanged from non-generational ZGC)
  • Improved throughput vs non-generational ZGC — gap with G1GC substantially narrowed
  • Enable in Java 21: -XX:+UseZGC -XX:+ZGenerational; default mode in Java 23+
  • Lower per-cycle memory overhead compared to non-generational ZGC
  • Self-tuning: minimal flag configuration required

What Is G1GC?

The Garbage-First Garbage Collector (G1GC) is the JDK default since Java 9. It is a generational, region-based collector that divides the heap into equal-sized regions and prioritizes collecting regions with the highest garbage density. Young generation collections are stop-the-world events running 10-100ms under typical configurations.

Java 21 also brought improvements to G1GC: better region management, improved handling of humongous objects, and further tuning refinements. G1GC in Java 21 is meaningfully better than G1GC in Java 17, though its fundamental architectural properties — including STW young generation collection — remain unchanged.

G1GC’s -XX:MaxGCPauseMillis is a soft target. Under memory pressure, the collector will exceed the target to maintain heap stability. This softness is acceptable for most workloads but is a fundamental limitation for applications with hard latency SLAs.

Key G1GC characteristics:

  • Generational, region-based; collects highest-garbage-density regions first
  • STW pauses for young gen collection: typically 10-100ms
  • -XX:MaxGCPauseMillis: soft pause time target (not a hard guarantee)
  • Highest throughput of the two; lowest continuous CPU overhead
  • Extensive tuning surface; well-understood across diverse workload types
  • Available and default in all JDK distributions

Generational ZGC vs G1GC Comparison

Feature Generational ZGC (Java 21+) G1GC
GC Algorithm Generational concurrent mark + relocate Generational region-based mark-compact
Stop-the-World Pauses Very brief (<1ms), not zero 10-100ms (young gen collections)
Latency Sub-millisecond at p99 10-200ms depending on config
Throughput Closer to G1GC than non-gen ZGC High — lowest overhead
Memory Overhead Moderate; lower than non-gen ZGC Lower than ZGC
Java Version Support Java 21+ (-XX:+ZGenerational); default in Java 23+ Default since Java 9
Best For Latency-sensitive Java 21+ workloads General-purpose workloads

Latency and Pause Times

The latency story between Generational ZGC and G1GC is the same as the broader ZGC vs G1GC story, now reinforced by Java 21 adoption. Generational ZGC maintains ZGC’s sub-millisecond STW pause times. G1GC maintains its 10-100ms pause profile. Generational mode did not change ZGC’s pause behavior — it changed its throughput efficiency.

What Java 21 added to G1GC on the latency front is marginal: better internal region management reduces worst-case pauses in some scenarios, and improved humongous object handling reduces the incidence of full GC fallback. But the fundamental STW young-gen pause profile is unchanged.

For Java 21 adopters evaluating GC selection: if your p99 latency requirement is under 50ms, both collectors can work — G1GC with appropriate tuning (-XX:MaxGCPauseMillis=50) and Generational ZGC without specific latency tuning. If your p99 requirement is under 10ms, only Generational ZGC is viable. If your requirement is under 1ms, you need to think carefully: Generational ZGC is close but still has brief STW phases.

A practical consideration for Java 21 deployments: Virtual Threads (Project Loom, now stable in Java 21) interact with GC in subtle ways. Virtual threads park onto carrier threads during blocking operations, so the number of GC roots does not scale with the number of virtual threads — both collectors handle this well. However, if your application uses massive numbers of virtual threads all becoming active simultaneously, the object allocation burst can stress any GC. ZGC’s self-tuning concurrent operation generally handles allocation bursts more gracefully than G1GC’s fixed-cycle approach.

Throughput and Memory Trade-offs

This is where the Java 21 Generational ZGC story is most compelling. Non-generational ZGC ran 10-15% lower throughput than G1GC on allocation-heavy workloads.

The mechanism is straightforward: instead of concurrently tracing the entire heap on every collection cycle, Generational ZGC frequently collects only the young generation (where allocation is concentrated) and infrequently scans the old generation. This matches the actual garbage distribution of most Java applications much better than uniform heap traversal.

For teams that previously ruled out ZGC due to its throughput overhead, Generational ZGC in Java 21 warrants a re-evaluation. The application that previously ran 12% slower with ZGC might now run 4% slower — a trade-off worth making for many latency-sensitive workloads.

Memory overhead slightly improves with Generational ZGC compared to non-generational ZGC. The generational bookkeeping adds some overhead, but the ability to collect the young generation more frequently reduces peak live-set size in the old generation, partially offsetting the bookkeeping cost.

Tuning: G1GC still requires more manual tuning to optimize. Key flags like -XX:G1NewSizePercent, -XX:G1MaxNewSizePercent, -XX:G1MixedGCLiveThresholdPercent, and -XX:MaxGCPauseMillis need careful calibration for latency-sensitive G1GC deployments. Generational ZGC handles these decisions automatically — the collector observes allocation rates and generational survival patterns and adjusts its behavior dynamically.

Introducing C4: The Truly Pauseless Alternative

Generational ZGC in Java 21 is a significant improvement. For many Java 21 adopters moving from G1GC to a lower-latency GC, it is the right answer. But it still has brief stop-the-world pauses — and for some workloads, that is still not acceptable.

Azul Prime’s C4 GC (Continuously Concurrent Compacting Collector) is designed to eliminate stop-the-world pauses. Where Generational ZGC achieves pause times under 1ms, C4 is designed to eliminate them. The Loaded Value Barrier (LVB) handles concurrent relocation without any STW phase.

The escalation path looks like this: G1GC for most workloads, Generational ZGC for Java 21+ latency-sensitive workloads, C4 when even Generational ZGC’s brief pauses exceed SLA requirements. Each step trades some operational simplicity for lower pause times — and C4 takes that to the logical conclusion.

C4 and Azul Prime (Zing) additionally offers:

  • Self-tuning operation — no GC flags to configure; the collector adjusts dynamically to workload
  • Multi-terabyte heap support with consistent sub-millisecond response times across all heap sizes
  • ReadyNow! technology — eliminates JIT warmup latency spikes at startup
  • Falcon JIT compiler — more aggressive optimizations than HotSpot C2, improving steady-state throughput
  • Drop-in compatibility with standard Java applications — no code changes required

Azul Prime is particularly compelling for Java 21 migrations: ReadyNow! addresses the JIT warmup problem that Java 21’s Virtual Threads can expose when spinning up large numbers of concurrent request handlers, and Falcon JIT’s optimizations compound over time in long-running services.

Frequently Asked Questions

How do I enable Generational ZGC in Java 21?

Use the flags -XX:+UseZGC -XX:+ZGenerational on your JVM launch command. In Java 23 and later, -XX:+ZGenerational is the default when ZGC is enabled. No other configuration is required — Generational ZGC is self-tuning. You can verify it is active by checking the GC log output, which will indicate ‘Generational ZGC’ as the active collector.

Is Generational ZGC better than G1GC for Java 21?

For latency-sensitive applications, yes. Generational ZGC provides sub-millisecond pause times while narrowing the throughput gap with G1GC to 3-8% in most workloads. For general-purpose applications where throughput is the primary metric, G1GC remains the better choice. The decision point is your p99 latency requirement: if it is under 10ms, Generational ZGC is superior.

Does Generational ZGC work with Virtual Threads in Java 21?

Yes. Both Generational ZGC and G1GC work correctly with Virtual Threads. GC root count does not scale with virtual thread count because virtual threads park onto carrier threads when blocked. Generational ZGC’s self-tuning concurrent operation handles the allocation bursts that high-concurrency virtual thread applications can produce particularly well.

What throughput improvement does Generational ZGC offer over non-generational ZGC?

The improvement varies by workload and on allocation-heavy workloads. Workloads with high short-lived object rates benefit most. The improvement comes from reducing unnecessary old-generation scanning on each collection cycle.

Should I migrate from G1GC to Generational ZGC with Java 21?

It depends on your latency requirements. If your application has explicit sub-10ms latency SLAs and you are migrating to Java 21, testing Generational ZGC is strongly recommended. If your application is primarily throughput-oriented or latency requirements are measured in hundreds of milliseconds, G1GC remains the practical default. Measure both in a load test environment representative of production before committing.

Conclusion

Generational ZGC in Java 21 represents a meaningful evolution of the ZGC vs G1GC trade-off. The throughput cost of choosing ZGC is now substantially lower than it was in Java 17 or earlier. For latency-sensitive Java 21 deployments, Generational ZGC offers a compelling combination of sub-millisecond pause times and G1GC-approaching throughput — it is no longer a niche choice for extreme latency requirements but a credible general-purpose GC for modern Java applications.

When the remaining brief STW pauses of Generational ZGC are still too much — when near-zero stop-the-world pauses are the requirement — Azul Prime’s C4 GC is designed to be the answer. Combined with ReadyNow! and the Falcon JIT, it offers a comprehensive solution for Java applications where the JVM itself is not intended to be the source of latency variability.