Definition
G1 is a balanced, general-purpose collector that targets a configurable pause time (default 200ms) and delivers good throughput for most server workloads. ZGC is a concurrent, low-latency collector whose pauses stay below roughly ten milliseconds, and in modern versions below one millisecond, regardless of heap size, because it does marking and compaction concurrently with the application. You choose ZGC when consistently tiny pauses are a hard requirement, such as real-time trading, and G1 when you want a solid all-round default with better raw throughput and lower CPU overhead.
Understanding the Concepts
When should you choose G1?
G1 does most marking concurrently but still evacuates (copies) objects during stop-the-world pauses. Its pause time grows somewhat with heap size and the amount of live data copied per collection, which is why its default target is 200 ms and why very large heaps can see longer pauses. In return, G1 has lower CPU overhead and generally higher throughput, and it is mature, well-understood, and the default. For the vast majority of services, G1 is the correct and safe choice.
# G1 for a general-purpose banking API
java -XX:+UseG1GC -XX:MaxGCPauseMillis=150 -Xms4g -Xmx4g -jar bank-api.jar
When should you choose ZGC?
ZGC is designed around one goal: keep pauses extremely short and roughly constant no matter how large the heap, from a few hundred megabytes to many terabytes. It achieves this by doing concurrent marking and, crucially, concurrent compaction using a technique called colored pointers and load barriers. Because the expensive work of relocating objects happens while the application runs, the stop-the-world portions are tiny. The trade-off is extra CPU spent on those barriers and slightly lower peak throughput, plus somewhat higher memory overhead.
# ZGC for a real-time trading engine
java -XX:+UseZGC -Xms16g -Xmx16g -jar trading-engine.jar
How do they compare?
| Property | G1 GC | ZGC |
|---|---|---|
| Pause time | 10-200 ms (configurable target) | Sub-millisecond (constant) |
| Throughput | Higher | Slightly lower |
| CPU overhead | Lower | Higher (load barriers) |
| Heap size sweet spot | 4-32 GB | Any size, even terabytes |
| Compaction | During stop-the-world | Concurrent |
| Default in | Java 9+ | Not default |
| Maturity | Very mature | Production-ready since Java 15 |
What is the decision rule?
The decision rule is about latency sensitivity and heap size.
package com.bank.ops;
/**
* GC selection decision tree:
*
* Is GC pause a significant part of your P99 latency?
* No → G1 (lower CPU overhead, higher throughput)
* Yes → Is the heap > 16 GB?
* No → G1 with a tighter MaxGCPauseMillis may suffice
* Yes → ZGC (concurrent compaction avoids heap-proportional pauses)
*
* Always validate with load testing, not intuition.
*/
public class GCDecisionGuide {
}
If your service has a strict, low tail-latency requirement – for example a real-time trading engine where a 200 ms pause during market hours is unacceptable – or if you run a very large heap where G1 pauses would grow uncomfortably long, choose ZGC (or Shenandoah, which is similar). If your service is a normal online API, a batch job, or anything where occasional pauses of tens to low hundreds of milliseconds are fine, choose G1 for its maturity and efficiency.
How do you validate the choice?
Never choose a low-latency collector reflexively. ZGC's benefit only matters if GC pauses are actually part of your latency problem, which you prove by reading GC logs and measuring your pause-time percentiles.
# Compare G1 and ZGC under the same load
# Step 1: Run with G1, collect GC logs
java -XX:+UseG1GC -Xlog:gc*:file=gc-g1.log -Xmx8g -jar bank-api.jar
# Load test → extract P99 pause from gc-g1.log
# Step 2: Run with ZGC, collect GC logs
java -XX:+UseZGC -Xlog:gc*:file=gc-zgc.log -Xmx8g -jar bank-api.jar
# Load test → extract P99 pause from gc-zgc.log
# Step 3: Compare P99, P999 pause times AND throughput
If G1 already meets your service-level objective, switching to ZGC just spends more CPU for no user-visible benefit. In banking at National Bank of India, Rajesh runs the customer API on G1 because customers never notice the small pauses, and reserves ZGC for the equity trading engine where every millisecond of freeze risks a missed order.
Key Takeaway
The choice between ZGC and G1 comes down to how sensitive the service is to pause time. G1 is the balanced, general-purpose default. It does much of its work concurrently but still copies objects during stop-the-world pauses, so its pause time grows a little with heap size and the amount of live data, and its default target is around two hundred milliseconds. In exchange, G1 is mature, efficient, and gives good throughput with low CPU overhead, which makes it the right choice for the vast majority of services. ZGC is a low-latency collector that keeps pauses in the low single-digit milliseconds, and in modern versions under a millisecond, no matter how large the heap, because it performs both marking and compaction concurrently with the application using colored pointers and load barriers. The price is extra CPU for those barriers and slightly lower peak throughput.
So the rule I apply is this: choose ZGC when the service has a strict, low tail-latency requirement, like a real-time trading engine where a two-hundred-millisecond freeze during market hours is simply unacceptable, or when the heap is so large that G1 pauses would grow uncomfortable. Choose G1 for everything else – normal online APIs, batch jobs, anything where occasional pauses of tens to low hundreds of milliseconds do no harm. And critically, I never switch to ZGC on a hunch. I prove GC is actually part of the latency problem by measuring pause-time percentiles under realistic load, because if G1 already meets the latency objective, moving to ZGC just burns CPU for no user benefit.
In my banking work at National Bank of India, this is exactly how we split our collectors. Our general account API runs on G1 because customers never notice the small pauses and we get efficient throughput. Our equity trading engine runs on ZGC because during market hours every millisecond of freeze risks a missed order worth real money, and ZGC's concurrent compaction keeps even the worst pauses around a millisecond. Before we made that switch, I load-tested both collectors and compared the 99th-percentile pause times against our latency budget, so the decision was backed by data rather than by reputation.
Remember one sentence: When should you choose ZGC vs G1: ZGC for ultra-low latency requirements (real-time systems, trading), G1 for general-purpose production workloads is fundamental knowledge for Java interviews and production banking work.
---