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

CollectorTypical Max PauseThroughputHeap ScalabilityDefault Since
G1GC~200ms (configurable)HighGood (<100GB)Java 9
ZGC<1msMedium-HighExcellent (up to 16TB)Java 21 (default option)
Shenandoah<1msMediumGoodNot 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

  1. Default choice: Start with G1GC. It's battle-tested and handles most workloads well.
  2. Latency is critical: Switch to ZGC (Java 21+) if your SLA demands single-digit millisecond response times.
  3. Already on Red Hat / OpenJDK with latency needs: Evaluate Shenandoah — it has a long track record in that ecosystem.
  4. 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.