Whether you’re comparing ZGC vs G1GC or G1GC vs ZGC, the analysis is the same — here’s a complete technical breakdown. The short answer: G1GC is the default Java garbage collector since Java 9, offering balanced throughput with configurable pause time goals (typically 10–200ms); ZGC is a low-latency alternative introduced in Java 11 (production-ready in Java 15) with sub-millisecond stop-the-world pauses. The right choice depends on your latency requirements, Java version, and workload characteristics.
Garbage collection is one of the most impactful variables in JVM performance. A poorly matched GC algorithm can cause unpredictable latency spikes, reduced throughput, or excessive memory overhead. Understanding the architectural differences between G1GC and ZGC — not just the marketing summaries — is essential for any Java engineer tuning a production system.
ZGC vs G1GC covers the algorithms, key configuration flags, real-world pause time behavior, throughput trade-offs, and decision criteria for both collectors. It also covers Generational ZGC (Java 21+), which significantly improves ZGC throughput and is now the recommended ZGC mode. For teams that need near-zero-pause garbage collection at any heap size, we cover Azul Prime’s C4 collector as a third option.
What Is G1GC?
G1GC (Garbage-First Garbage Collector) became the default JVM garbage collector in Java 9, replacing the older Parallel GC as the out-of-the-box choice. It was designed to provide a balance between throughput and latency, with a soft pause-time goal that the JVM attempts to meet on a best-effort basis.
Architecturally, G1GC divides the heap into a configurable number of equal-sized regions (1–32MB, always a power of two, controlled by -XX:G1HeapRegionSize). Unlike older generational collectors that use fixed young/old generation boundaries, G1GC dynamically assigns regions as Eden, Survivor, or Old. When collection is triggered, it prioritizes (“Garbage First”) the regions with the most garbage, maximizing reclaimed space per pause.
G1GC collection phases include: concurrent marking (runs alongside application threads to identify live objects), minor GCs (stop-the-world, evacuate young regions), mixed GCs (evacuate young + selected old regions), and full GCs (fallback, stop-the-world, should be rare). The -XX:MaxGCPauseMillis flag sets a soft pause target — the JVM will adjust the collection set size to try to stay within this target, but it is not a hard guarantee.
Key configuration flags for G1GC:
- -XX:+UseG1GC — enable G1GC (default since Java 9; explicit flag still accepted)
- -XX:MaxGCPauseMillis=200 — soft pause time goal in milliseconds (default 200ms)
- -XX:G1HeapRegionSize=n — region size in MB; must be power of 2 between 1 and 32
- -XX:G1NewSizePercent=5 / -XX:G1MaxNewSizePercent=60 — young gen sizing bounds
- -XX:ConcGCThreads=n — concurrent marking threads
- -XX:ParallelGCThreads=n — STW parallel GC threads
- -XX:+UseStringDeduplication — reduce memory for duplicate String objects
G1GC performs well on heaps from a few gigabytes up to about 16–32GB. Beyond that, full GC fallback pauses can become very long, and ZGC or Shenandoah become more appropriate.
What Is ZGC?
ZGC (Z Garbage Collector) is a scalable, low-latency garbage collector developed by Oracle and first shipped as experimental in JDK 11. It became production-ready in Java 15 and saw a major architectural upgrade with Generational ZGC in Java 21.
ZGC’s defining characteristic is that almost all GC work happens concurrently with the running application. It uses load barriers — code injected at object reference reads — to track pointer state and perform concurrent relocation and remapping. Stop-the-world pauses in ZGC are typically under 1 millisecond, regardless of heap size, because STW phases only perform root scanning (scanning thread stacks and other GC roots), not object evacuation or compaction.
ZGC supports heap sizes from a few hundred megabytes up to 16 terabytes. Because relocation happens concurrently, pause times do not scale with heap size — a 1TB ZGC heap has the same sub-millisecond pause characteristics as a 4GB heap. This is fundamentally different from G1GC, where pause duration correlates with the amount of live data being evacuated.
Generational ZGC (Java 21+, enabled with -XX:+ZGenerational) adds a generational model to ZGC. Classic ZGC was non-generational — it treated all objects equally regardless of age — which meant it allocated more CPU to scanning long-lived objects unnecessarily. Generational ZGC applies the weak generational hypothesis (most objects die young) to reduce GC overhead and improve throughput substantially. As of Java 21, Generational ZGC is the recommended mode; the non-generational mode is retained but may be deprecated in future releases.
Key configuration flags for ZGC:
- -XX:+UseZGC — enable ZGC
- -XX:+ZGenerational — enable Generational ZGC (Java 21+, strongly recommended)
- -XX:ConcGCThreads=n — number of concurrent GC threads (ZGC auto-tunes if unset)
- -XX:ZAllocationSpikeTolerance=2.0 — multiplier for allocation spike headroom
- -Xlog:gc* — enable GC logging for monitoring pause times and cycle frequency
ZGC requires more memory overhead than G1GC due to multi-mapping (each object is mapped at multiple virtual addresses) and the load barrier infrastructure, but the trade-off is near-elimination of GC-induced latency.
ZGC vs G1GC: Head-to-Head
The following table summarizes the key technical differences between ZGC and G1GC across the dimensions that matter most for production decisions:
| GC Algorithm | G1GC | ZGC |
|---|---|---|
| STW Pauses | Yes — multiple phases | Minimal — root scan only |
| Typical Pause Duration | 10–200ms (tunable) | <1ms regardless of heap |
| Throughput | Higher (lower concurrent overhead) | Lower (concurrent work overhead) |
| Memory Overhead | Low–moderate | Moderate–high (multi-mapping) |
| Heap Size Limit | Practical limit ~32GB before long full GCs | Up to 16TB |
| Java Version | Default since Java 9 | Experimental Java 11; Production Java 15 |
| Generational Mode | Always generational | Java 21+ with -XX:+ZGenerational |
| Best For | General-purpose; throughput-sensitive apps | Latency-sensitive; large heaps |
Pause Times in Practice
Understanding what pause times actually mean in production requires going beyond benchmarks. A “200ms pause goal” in G1GC means that the JVM attempts to keep individual GC pauses to 200ms, adjusting how many heap regions it collects per cycle. In practice, most minor GC pauses on reasonably-sized heaps land in the 10–50ms range. However, there are three scenarios where G1GC pauses exceed this target significantly.
First, concurrent marking humongous allocation: when a single object exceeds 50% of a region size, it is classified as “humongous” and allocated directly in old generation regions, bypassing normal young gen collection. Heavy humongous allocation can disrupt G1’s region management and lead to longer pauses. Second, full GC fallback: if G1GC cannot keep up with allocation rate — for example, concurrent marking does not complete before the next GC cycle is needed — it falls back to a serial full GC. On large heaps, this can mean pause times of several seconds. Third, mixed GC collection set sizing: when G1 miscalculates the live data ratio in old generation regions, it may collect more data than fits in the pause budget, causing overruns.
ZGC’s sub-1ms pauses come from a deliberate architectural decision: STW phases only scan GC roots (thread stacks, JNI references, etc.). This is a fixed-size task that does not grow with heap occupancy or live data volume. Relocation of objects — the expensive part — happens concurrently. Load barriers in application code intercept object reference reads and perform pointer fixup on the fly, allowing ZGC to remap relocated objects without a STW phase.
Under high allocation load, ZGC’s pause times remain stable because the bottleneck shifts to concurrent GC thread capacity, not STW phases. If concurrent GC threads cannot keep up with allocation, ZGC’s response is to stall allocation threads briefly — a behavior called “allocation stalls” that differs from traditional STW pauses but can still appear as latency in application metrics. Monitoring -Xlog:gc* output for “Allocation Stall” events is important when running ZGC under heavy load.
For latency SLAs of 50ms or lower, ZGC is almost always the right choice. For SLAs measured in hundreds of milliseconds, G1GC often delivers adequate latency with better throughput. For truly zero-pause requirements at any heap scale, neither open-source collector fully delivers — see the section on Azul Prime’s C4 below.
Throughput Trade-offs
Throughput — the fraction of CPU time spent running application code rather than GC — favors G1GC in most workloads. G1GC’s stop-the-world phases are highly parallelized and relatively brief for typical heap sizes, so total GC overhead is generally 5–10% of CPU. ZGC’s concurrent phases, by contrast, consume CPU continuously alongside the application, even between GC cycles when G1 would be idle.
In benchmark environments (SPECjbb, Renaissance), G1GC typically outperforms ZGC on raw throughput by 5–15%, depending on object allocation patterns. Generational ZGC (Java 21+) narrows this gap significantly, making it competitive in more scenarios.
The throughput penalty of ZGC matters most in CPU-bound workloads with moderate latency requirements. Batch processing, data transformation pipelines, and CPU-intensive computation see more benefit from G1GC’s lower GC overhead. Network I/O-bound services — where threads frequently block on I/O — typically have spare CPU headroom that absorbs ZGC’s concurrent overhead without measurable throughput loss.
When evaluating trade-offs, measure under realistic load. Synthetic benchmarks rarely capture the allocation patterns of production workloads. Use Java Flight Recorder (JFR) to capture GC pause histograms and concurrent GC CPU usage in your specific application before committing to either collector.
When to Use G1GC
G1GC is the right default choice for the majority of Java applications. Choose G1GC when:
- Your latency requirements are in the 50–500ms range — G1’s pause goal of 100–200ms is sufficient
- Your heap size is 4–16GB — G1 performs reliably in this range with well-understood behavior
- You need maximum throughput from your CPU budget
- You’re running Java 9–14 where ZGC is not yet production-ready
- You’re operating a mixed workload with both latency-sensitive and throughput-sensitive components
- Your team is more familiar with G1GC tuning and you want well-documented, widely-understood behavior
G1GC works particularly well for: web application servers (Spring Boot, Jakarta EE), microservices with moderate request rates, batch jobs where throughput matters more than tail latency, and applications with stable, predictable allocation rates.
If you find G1GC pause times exceeding your SLA targets, the first step is to measure — use -Xlog:gc*:file=gc.log to capture pause data, then examine whether pauses are from minor GC, mixed GC, or full GC. Minor GC overruns often indicate humongous allocation or young gen sizing issues. Full GC events indicate G1 cannot keep up with allocation rate and warrant either increasing heap, reducing allocation rate, or switching to ZGC.
When to Use ZGC
ZGC is the right choice when latency consistency is a primary requirement, not just a nice-to-have. Choose ZGC when:
- You need pause times consistently below 10ms or ideally below 1ms
- You are running Java 15+ and latency SLAs are stringent
- Your heap exceeds 16–32GB, where G1GC full GC fallback pauses become unacceptable
- You run on Java 21+ and want Generational ZGC for improved throughput with low latency
- Your application is I/O-bound and has CPU headroom to absorb ZGC’s concurrent overhead
- You’re building real-time or near-real-time systems: trading platforms, gaming servers, streaming processing, interactive APIs with strict p99 targets
On Java 21 and later, we recommend enabling Generational ZGC: -XX:+UseZGC -XX:+ZGenerational. This provides meaningfully better throughput than classic ZGC while preserving the sub-millisecond pause characteristics. Oracle intends for Generational ZGC to eventually become the standard ZGC mode.
Be aware that ZGC requires more heap headroom than G1GC. Because concurrent relocation requires objects to be reachable from two locations simultaneously during a GC cycle, ZGC needs approximately 10–25% extra heap compared to what G1GC needs for the same workload. Set -Xmx with sufficient headroom and monitor allocation stall frequency in logs.
Beyond ZGC and G1GC: Azul Prime’s C4
Both ZGC and G1GC represent the state of the art in open-source JVM garbage collection, but neither is truly pauseless. ZGC’s “sub-millisecond” pauses are real improvements, but root scanning STW phases still occur, and allocation stalls under high load can introduce latency that shows up in p99.9 measurements.
Azul Prime ships the C4 garbage collector (formerly Zing) — the Continuously Concurrent Compacting Collector. C4 is designed to be pauseless: it performs concurrent relocation, concurrent compaction, and concurrent reference processing with near-zero stop-the-world phases. Unlike ZGC’s load barrier approach, C4 uses the Loaded Value Barrier (LVB), a read-barrier-based scheme that enables full concurrent operation at any heap size.
C4 scales from small heaps to multi-terabyte deployments without pause time degradation. It is self-tuning: unlike G1GC and ZGC which benefit from manual flag configuration, C4 adapts its concurrency level and GC pacing automatically based on observed allocation rate and heap occupancy. Most teams running Azul Prime need only set -Xms and -Xmx; the GC handles the rest.
Azul Prime also ships the Falcon JIT compiler (LLVM-based, more aggressive optimizations than HotSpot C2 and ReadyNow! (which substantially reduces JIT warmup by caching compilation profiles across restarts). For teams where GC pause predictability is a competitive requirement — financial services, real-time analytics, high-frequency trading infrastructure — Azul Prime is designed to provide consistency that open-source JVM GC algorithms typically cannot match. Learn more at https://www.azul.com/products/prime/
Frequently Asked Questions
Is ZGC better than G1GC?
It depends on your workload. ZGC delivers far lower and more consistent pause times (sub-1ms vs. 10–200ms for G1GC), making it better for latency-sensitive applications. G1GC typically delivers higher throughput and is the better default for general-purpose workloads. Neither is universally “better.”
When should I switch from G1GC to ZGC?
Switch when GC pauses are causing SLA violations and are measurably impacting user-facing latency metrics. Before switching, confirm through GC logs that GC pauses (not application code or I/O) are the actual bottleneck. ZGC is most beneficial when you’re on Java 15+, your heap is large, and your workload has CPU headroom.
What is Generational ZGC?
Generational ZGC, introduced in Java 21 with -XX:+ZGenerational, adds a young/old generation model to ZGC. Classic ZGC was non-generational — it did not distinguish short-lived from long-lived objects. Generational ZGC collects young objects more frequently, reducing overall GC overhead and improving throughput by 10–20% while preserving ZGC’s sub-millisecond pause characteristics.
Does ZGC have stop-the-world pauses?
Yes, but they are extremely brief — typically under 1 millisecond. ZGC’s STW phases are limited to root scanning (thread stacks and JNI references). Object evacuation, relocation, and compaction all happen concurrently. This contrasts with G1GC, where evacuation is stop-the-world. Under high allocation load, ZGC may produce allocation stalls which are not traditional STW pauses but can appear as latency in application metrics.
How do I enable ZGC in Java 21?
Use the following flags: -XX:+UseZGC -XX:+ZGenerational. In Java 21, Generational ZGC is strongly recommended over classic ZGC. For logging: add -Xlog:gc*:file=gc.log:time,uptime. Set heap size with -Xms and -Xmx to equal values to prevent heap resizing. Heap headroom of 25% beyond your working set is advisable.
What GC does Azul Prime use?
Azul Prime uses C4 — the Continuously Concurrent Compacting Collector. C4 is a near-zero-pause-time GC that performs all phases concurrently, including compaction. It is self-tuning, scales to terabyte heaps, and ships as part of the Azul Prime JVM alongside the Falcon JIT compiler and ReadyNow! warmup reduction.
Is there a truly pauseless Java GC?
Among open-source JVMs, ZGC comes closest with sub-millisecond pauses, but it does have brief STW root scanning phases. Shenandoah (Red Hat) follows a similar concurrent approach with comparable pause characteristics. Among commercially available options, C4 in Azul Prime is designed to be pauseless, addressing stop-the-world phases through its concurrent compaction architecture.
Conclusion
Choosing between ZGC and G1GC comes down to one question: are your latency requirements driven by tail latency consistency, or by average throughput? G1GC is the right default for most workloads — it is mature, well-understood, and delivers balanced performance with configurable pause targets. ZGC, especially Generational ZGC in Java 21, is the right choice when pause time predictability at the p99 or p99.9 level is non-negotiable.
For teams running large heaps, latency-critical workloads, or services where GC pause spikes translate directly to revenue impact or SLA penalties, Azul Prime’s C4 collector substantially reduces the pause time trade-off. With self-tuning behavior, the Falcon JIT, and ReadyNow! warmup reduction, Azul Prime is engineered for the production environments where GC behavior matters most.