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

ZGC vs G1GC Java 21

Smart Summary

Java 21 brings Generational ZGC and further G1GC improvements, changing the trade-offs between the two collectors. This guide compares ZGC and G1GC in the Java 21 LTS context, including their interaction with Virtual Threads, and explains where Azul Prime’s C4 goes further.

Introduction

Java 21 is a Long-Term Support release and a natural decision point for GC selection. It introduced Generational ZGC as a production-ready feature — the most significant GC evolution since ZGC’s promotion to production in Java 15. For engineering teams adopting Java 21 LTS, the ZGC vs G1GC question now has a materially different answer than it did in Java 17.

This article examines ZGC and G1GC specifically in the Java 21 context: what changed in each collector, how they interact with Java 21’s new features (particularly Virtual Threads), and how to choose between them. For workloads where neither collector’s pause profile meets production SLAs, we also cover Azul Prime’s C4 GC.

What Is ZGC in Java 21?

ZGC in Java 21 means Generational ZGC — the culmination of multi-year engineering effort to apply the generational hypothesis to ZGC’s concurrent architecture. Before Java 21, ZGC treated all heap objects uniformly. Generational ZGC divides the heap into young and old generations, applying ZGC’s concurrent GC mechanics to each.

To use Generational ZGC in Java 21: -XX:+UseZGC -XX:+ZGenerational. In Java 23 and later, generational mode is the default when UseZGC is active.

The practical impact: Generational ZGC is faster than non-generational ZGC for most allocation-heavy workloads. STW pause times remain under 1ms and remain nearly independent of heap size — the core ZGC promise is unchanged, but the throughput cost of that promise is significantly lower.

Java 21 ZGC key facts:

  • Enable with -XX:+UseZGC -XX:+ZGenerational (or use Java 23+ where it is the default)
  • STW pauses: under 1ms, heap-size-independent
  • Throughput: 3-8% below G1GC on most workloads (improved from prior ZGC versions)
  • Self-tuning: minimal flag configuration required
  • Scales to multi-terabyte heaps
  • Works correctly with Virtual Threads — GC root count does not scale with virtual thread count

What Is G1GC in Java 21?

G1GC remains the JDK default in Java 21. Java 21 brought several internal G1GC improvements: better region management algorithms, improved humongous object handling (reducing the frequency of fallback to full GC for large object allocation), and refined mixed GC heuristics. These are meaningful improvements, but they do not change G1GC’s fundamental architecture.

Young generation collection in Java 21 G1GC is still a stop-the-world event running 10-100ms under typical configurations. The -XX:MaxGCPauseMillis target is still soft — the collector will exceed it under pressure. Full GC fallback, while less frequent in Java 21, still exists and can cause multi-second pauses in extreme cases.

G1GC in Java 21 also interacts well with Virtual Threads. The critical property is that virtual threads park onto carrier threads during blocking operations, so the effective GC root count is bounded by carrier thread count (typically cpu_count), not virtual thread count. This means G1GC pause times do not blow up with high virtual thread concurrency — a concern early adopters often raise.

Java 21 G1GC key facts:

  • Default GC — no flags required
  • STW pauses for young gen: typically 10-100ms (-XX:MaxGCPauseMillis is a soft target)
  • Highest throughput of the two
  • Java 21 improvements: better region management, improved humongous object handling
  • Works correctly with Virtual Threads
  • Extensive tuning surface for workload-specific optimization

ZGC vs G1GC Java 21 Comparison

Feature ZGC (Java 21) G1GC (Java 21)
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 prior Java versions Highest of the two
Memory Overhead Moderate Lower than ZGC
Java Version Support Java 21 (generational mode); Java 23+ default Default since Java 9; improved in Java 21
Best For Latency-sensitive Java 21 LTS workloads General-purpose Java 21 workloads

Latency and Pause Times

For Java 21 adopters, the pause time comparison between ZGC and G1GC is unchanged from the fundamental architectural analysis: ZGC pauses are under 1ms; G1GC pauses are 10-100ms. Java 21 made G1GC incrementally better but did not change its STW pause profile. Java 21 made ZGC’s generational mode production-ready, improving its throughput without changing its pause characteristics.

The practical implication for Java 21 LTS adoption: if you are migrating from Java 11 or Java 17 and your application was on G1GC, you should benchmark Generational ZGC. The throughput cost is now low enough that the sub-millisecond pause benefit may be worth it even for applications that previously stayed on G1GC.

Virtual Threads (Project Loom, stable in Java 21) add a nuance to latency analysis. When massive numbers of virtual threads unpark simultaneously — for example, when a cache refresh completes and thousands of waiting threads resume — the resulting allocation burst can stress any GC. ZGC’s concurrent operation adapts more gracefully to sudden allocation spikes than G1GC’s fixed-region-cycle approach. This is not a hard rule but a workload-specific consideration worth benchmarking.

Heap size in Java 21 deployments: Java 21 LTS encourages larger deployments — consolidating microservices onto fewer JVM instances to reduce overhead. For heaps above 64GB, ZGC’s heap-size-independent pause times become increasingly relevant. G1GC pause times on a 256GB heap are materially longer than on a 16GB heap; ZGC pauses are not.

Throughput and Memory Trade-offs

Java 21 is the first release where ZGC vs G1GC is a genuine trade-off rather than a clear G1GC throughput win. For applications where 5% throughput is affordable in exchange for sub-millisecond latency, the trade-off now favors ZGC.

The throughput improvement comes from reduced GC work per cycle. Non-generational ZGC had to trace the entire heap on each collection. Generational ZGC traces the young generation frequently — where most allocation occurs — and the old generation infrequently. This matches actual application behavior and reduces the total CPU cost of GC.

Virtual Threads amplify the throughput trade-off analysis. Applications using Virtual Threads often have much higher concurrency than thread-per-request applications, which means higher allocation rates from concurrent request processing. ZGC’s self-tuning concurrent operation — scaling GC thread count to match allocation pressure — handles this more gracefully than G1GC’s fixed region-size assumptions.

Memory overhead: ZGC uses more memory than G1GC due to colored pointer metadata. On Java 21 deployments with large heaps (100GB+), this overhead is non-trivial. For memory-constrained environments, G1GC remains the more efficient choice. For latency-constrained environments with adequate memory, Generational ZGC’s overhead is acceptable.

Operational overhead: Generational ZGC is self-tuning. G1GC requires more manual calibration. For Java 21 LTS deployments that will run for years with minimal GC tuning revisits, ZGC’s self-tuning property reduces operational burden.

Introducing C4: The Truly Pauseless Alternative

Java 21’s Generational ZGC is genuinely impressive — one of the best versions of ZGC to date, and a credible alternative to G1GC for latency-sensitive workloads. But it still has brief stop-the-world pauses, and for some production environments, that is not acceptable.

Azul Prime’s C4 GC (Continuously Concurrent Compacting Collector) is designed to eliminate stop-the-world pauses. The Loaded Value Barrier (LVB) handles concurrent object relocation without any STW phase. Application threads are designed to avoid pausing for GC — not for 200 microseconds, not for any meaningful duration.

For Java 21 adopters specifically, Azul Prime offers additional advantages beyond pause elimination. ReadyNow! technology addresses the JIT warmup challenge that is especially visible in Java 21 deployments using Virtual Threads: when a new JVM instance starts handling high-concurrency traffic, HotSpot needs time to JIT-compile hot paths. ReadyNow! captures compilation state and replays it on startup, giving consistent performance from the first request.

C4 and Azul Prime (formerly 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 — substantially reduces 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

The decision to adopt Azul Prime is not typically made because ZGC is bad — it is made because the application’s latency requirements are stringent enough that the remaining brief ZGC pauses are a production risk. Financial trading platforms, real-time risk engines, and high-frequency API infrastructure fall into this category. For those environments, C4’s pause-elimination design is typically the most reliable answer.

Frequently Asked Questions

What changed for ZGC in Java 21?

Java 21 promoted Generational ZGC to production-ready status. Generational ZGC adds a young/old generation split to ZGC’s concurrent architecture, substantially improving throughput on allocation-heavy workloads while preserving ZGC’s sub-millisecond pause times. Enable with -XX:+UseZGC -XX:+ZGenerational. In Java 23, generational mode is the default when ZGC is selected.

Should I use ZGC or G1GC for a Java 21 LTS deployment?

Use Generational ZGC if your application has latency requirements under 10ms or if your heap is above 64GB. Use G1GC if throughput is the primary metric and your latency requirements are met by pause times in the 10-100ms range. For new Java 21 deployments, benchmark both — the throughput gap has narrowed enough that ZGC is viable for a broader range of applications than it was in Java 17.

How do Virtual Threads interact with GC in Java 21?

Virtual threads park onto carrier threads during blocking operations, so the GC root count is bounded by carrier thread count, not virtual thread count. This means both ZGC and G1GC handle high virtual thread concurrency without proportionally longer GC pauses. The more relevant interaction is allocation rate: applications with thousands of concurrent virtual threads may have high allocation rates that stress GC throughput — Generational ZGC’s self-tuning concurrent operation handles this well.

Is ZGC or G1GC better for large heaps in Java 21?

ZGC is better for large heaps (above 64GB). G1GC pause times grow with live set size, making multi-hundred-GB heap pauses increasingly severe. ZGC pause times are nearly independent of heap size — a 512GB heap under ZGC produces the same sub-millisecond pause as a 16GB heap. For Java 21 deployments with large heaps, ZGC is strongly preferred.

What is the GC situation for Java 21 LTS in production?

Java 21 LTS offers two strong GC options: G1GC (default, best throughput, 10-100ms pauses) and Generational ZGC (opt-in, sub-millisecond pauses, 3-8% throughput overhead). Both are production-ready and well-supported. Shenandoah is also available in OpenJDK but not Oracle JDK. For near-zero-pause requirements, Azul Prime’s C4 GC is available as a commercial option with a free trial.

Conclusion

Java 21 LTS is an excellent time to re-evaluate your GC selection. Generational ZGC’s promotion to production-ready status in Java 21 makes ZGC a credible alternative to G1GC for a broader range of applications than ever before. For latency-sensitive workloads, Java 21 LTS deployments with large heaps, or applications using Virtual Threads with high concurrency, Generational ZGC deserves serious consideration alongside G1GC.

When pause elimination — not just minimization — is the requirement, Azul Prime’s C4 GC provides a near-zero-STW-pause production JVM option, complemented by ReadyNow! for consistent Java 21 startup performance and the Falcon JIT for optimized steady-state throughput.