Why Garbage Collection Still Matters
Despite decades of improvements, garbage collection (GC) remains one of the most impactful variables in Java application performance. A poorly configured GC can cause multi-second pause times in production, tank your P99 latencies, or cause Kubernetes liveness probes to fail. Understanding how modern collectors work — and how to choose between them — is an essential skill for any JVM developer.
The Three Modern Collectors
G1GC (Garbage-First Garbage Collector)
G1GC has been the default collector since Java 9. It divides the heap into equal-sized regions and prioritizes collecting the regions with the most garbage first (hence "Garbage-First"). G1 aims to meet a user-configurable pause time goal (-XX:MaxGCPauseMillis).
- Pause goal: Configurable (default 200ms)
- Heap size sweet spot: 6GB–100GB+
- Best for: General-purpose workloads balancing throughput and latency
ZGC (Z Garbage Collector)
ZGC is a concurrent, low-latency collector available since Java 11 (production-ready since Java 15). Almost all GC work happens concurrently with application threads, resulting in pause times that are essentially independent of heap size — typically staying under 1ms.
- Pause goal: Sub-millisecond (usually <1ms)
- Heap size range: 8MB to 16TB
- Best for: Latency-sensitive applications (trading platforms, real-time APIs, gaming backends)
- Trade-off: Slightly lower maximum throughput than G1 due to concurrent overhead
Shenandoah GC
Shenandoah, developed by Red Hat, takes a similar approach to ZGC — performing most GC work concurrently. It uniquely performs concurrent compaction, which ZGC does not (ZGC compacts concurrently as of Java 21). Shenandoah is available in OpenJDK builds.
- Pause goal: Sub-millisecond
- Best for: Applications that need consistent low latency with moderate heap sizes
- Trade-off: Higher CPU overhead; less suitable for throughput-bound workloads
Quick Comparison
| Collector | Typical Max Pause | Throughput | Heap Scalability | Default Since |
|---|---|---|---|---|
| G1GC | ~200ms (configurable) | High | Good (<100GB) | Java 9 |
| ZGC | <1ms | Medium-High | Excellent (up to 16TB) | Java 21 (default option) |
| Shenandoah | <1ms | Medium | Good | Not default |
Key JVM Flags to Know
-XX:+UseG1GC— Enable G1 (default from Java 9)-XX:+UseZGC— Enable ZGC-XX:+UseShenandoahGC— Enable Shenandoah-XX:MaxGCPauseMillis=100— G1 pause time hint-Xms/-Xmx— Set initial and max heap size-XX:+PrintGCDetails -Xlog:gc*— Enable GC logging for analysis
How to Choose
- Default choice: Start with G1GC. It's battle-tested and handles most workloads well.
- Latency is critical: Switch to ZGC (Java 21+) if your SLA demands single-digit millisecond response times.
- Already on Red Hat / OpenJDK with latency needs: Evaluate Shenandoah — it has a long track record in that ecosystem.
- Always measure: Use tools like JFR (Java Flight Recorder), VisualVM, or GCViewer to analyze actual GC behavior in your workload before changing collectors in production.
Conclusion
Modern Java GC has come a long way. For most applications, G1GC is the right default. For latency-sensitive services, ZGC in Java 21+ is now a compelling, well-supported choice with minimal configuration overhead. The key is to measure first, tune second — never guess at GC behavior without data.