Contents
GC Fundamentals G1GC Architecture G1GC Tuning & Log Analysis ZGC Architecture ZGC Tuning Shenandoah GC GC Comparison Table Choosing the Right GC GC Log Patterns to Recognise
Before comparing collectors, it helps to understand the core concepts that all of them build on. These terms appear constantly in GC documentation and logs.
- Generational hypothesis — most objects die young. Collectors exploit this by separating short-lived objects (Young Gen / Eden) from long-lived ones (Old Gen / Tenured). Young GC is cheap because it only scans a small region.
- Stop-the-world (STW) pause — the JVM pauses all application threads while the GC works. All collectors have at least some STW phases; the goal of modern GCs is to minimise their frequency and duration.
- Concurrent phase — GC work runs alongside application threads. Requires careful coordination because the heap can change while the GC is scanning it (write barriers handle this).
- Write barrier — a small piece of code the JIT injects into every heap write. It notifies the GC when a reference changes, keeping the GC's tracking data accurate during concurrent phases.
- Promotion — an object that survives enough Young GCs is moved to Old Gen. Premature promotion (objects promoted before they die) is a common G1GC performance problem.
- Humongous object (G1) — an object larger than half a region. G1 allocates these directly in Old Gen, bypassing Young Gen completely, which can cause problems if they are short-lived.
G1GC (Garbage-First Garbage Collector, default since Java 9) divides the heap into equal-sized regions (1–32 MB each, always a power of two). Each region is dynamically assigned a role: Eden, Survivor, Old, or Humongous. This removes the hard boundary between generations that plagued CMS.
- Young GC — runs when Eden is full. Collects all Eden and Survivor regions (STW, parallel). Live objects are copied to new Survivor or Old regions. This is fully stop-the-world but fast (typically <10 ms).
- Concurrent marking cycle — triggered when heap occupancy reaches IHOP (default 45%). Runs mostly concurrently: Initial Mark (piggybacks on Young GC), Root Region Scan, Concurrent Mark, Remark (STW), Cleanup (STW for accounting, concurrent for freeing empty regions).
- Mixed GC — after marking, G1 selects a mix of Young regions plus Old regions with the most garbage (hence "Garbage-First"). Runs as a series of incremental STW pauses.
- Full GC — last resort. Single-threaded (prior to Java 10), then parallel (Java 10+). Occurs when G1 cannot reclaim space fast enough. A Full GC is a sign of misconfiguration or a memory leak.
G1GC is designed to be self-tuning around a pause time goal. The most effective approach is to set the goal and let G1 adapt, only overriding specific behaviours when GC logs show a concrete problem.
Reading the G1GC log — key lines to watch:
Tuning actions based on log signals:
| Log signal | Cause | Fix |
|---|---|---|
| Full GC (Ergonomics) | IHOP too high — marking starts too late | Lower |
| Full GC (Humongous) | Short-lived large objects allocated in Old Gen | Increase |
| To-space exhausted | Allocation rate exceeds evacuation rate | Increase heap size; reduce |
| Young GC > pause goal | Young Gen too large | Lower |
| Old Gen filling despite GC | Promotion rate high — objects not dying young | Check for large short-lived object allocations (allocation profiler) |
ZGC (Z Garbage Collector, production since Java 15, generational since Java 21) achieves sub-millisecond pauses by doing virtually all work concurrently — including relocation (compaction). The key innovation is colored pointers: the top bits of every 64-bit object reference encode GC metadata (mark state, relocation status). This eliminates the need to stop threads to check object states during concurrent collection.
- Load barriers — the JIT injects a small check on every heap reference read. When a thread loads a pointer, the barrier checks the color bits and performs any needed fixup (e.g., following a forwarding pointer to the new object location). This keeps threads synchronized with the GC without stopping them.
- Concurrent relocation — ZGC moves live objects while the application runs. Load barriers transparently redirect reads to the new location until all pointers are updated.
- Generational ZGC (Java 21+) — adds Young/Old generation separation to ZGC. Young gen collections are even cheaper, significantly improving throughput without sacrificing pause goals. Enable with
-XX:+ZGenerational . - Pause phases — ZGC has three very short STW pauses per cycle: Pause Mark Start, Pause Mark End, and Pause Relocate Start. All typically complete in under 1 ms.
ZGC is largely self-tuning and requires far less manual configuration than G1GC. The most important decisions are heap sizing and the soft max heap size.
ZGC log — key lines:
ZGC tuning rules:
- If you see Allocation Stall, the allocation rate is outpacing the GC. First try increasing heap size. If the heap is already large, reduce allocation rate (profiler).
- Set
-XX:SoftMaxHeapSize to ~85% of-Xmx to leave headroom for allocation spikes without triggering emergency GC. - ZGC uses more CPU than G1GC because concurrent work runs alongside the application. Do not use ZGC on CPU-constrained containers without testing throughput impact.
Shenandoah achieves sub-millisecond pauses using a different technique than ZGC: Brooks forwarding pointers. Each object has an extra header word that initially points to itself. When the GC relocates an object, the forwarding pointer in the old location is updated to point to the new one. Any thread that reads through the old pointer follows the forwarding chain transparently.
- Available in OpenJDK (since Java 12) and all Red Hat / Amazon Corretto JDKs
- No colored pointers — works on 32-bit references and compressed oops unlike ZGC
- Heuristics-driven:
adaptive (default),compact (aggressive, low-footprint),static (fixed-interval) - Typical pause times: 1–5 ms (similar to ZGC, slightly higher overhead per collection)
A side-by-side view of all production-grade collectors to help with selection decisions:
| Feature | Serial / Parallel | G1GC | ZGC | Shenandoah |
|---|---|---|---|---|
| Available since | Java 1 / Java 1.3 | Java 7 (default Java 9) | Java 11 (prod Java 15) | Java 12 |
| Max pause | Seconds (STW all phases) | 10–200 ms (tunable) | <1 ms | 1–5 ms |
| Throughput | Highest (no concurrent overhead) | High | Moderate (load barriers) | Moderate |
| Heap scale | Small–medium | 4–32 GB optimal | Up to terabytes | 4–64 GB |
| GC threads | Parallel, STW only | Parallel STW + concurrent | Mostly concurrent | Mostly concurrent |
| Generational | Yes | Yes | Yes (Java 21+) | Partial (in progress) |
| JDK availability | All JDKs | All JDKs | OpenJDK, GraalVM | OpenJDK, Corretto, Red Hat |
| Best for | Batch / single-core containers | General-purpose APIs | Ultra-low latency, huge heaps | Low latency on Red Hat JDKs |
There is no universally best GC. The decision tree below covers the most common production scenarios:
- Container with <1 GB heap, single core — use
-XX:+UseSerialGC . Parallel threads cost more than they save at this scale. - Batch job, throughput matters, latency does not — use
-XX:+UseParallelGC . Maximises CPU usage for collection, shortest total GC time. - REST API, heap 2–32 GB, pause goal 50–200 ms — use G1GC (default). Well-understood, well-tooled, self-tuning around
-XX:MaxGCPauseMillis . - Low-latency service (trading, gaming, real-time), pause goal <10 ms — use ZGC with
-XX:+ZGenerational on Java 21+. Also consider Shenandoah on Red Hat JDKs. - Very large heap (>32 GB) — use ZGC. G1GC pause times grow with heap size; ZGC pauses remain sub-millisecond regardless of heap size.
A short reference to the most important patterns in unified GC logs (
| Pattern | Meaning | Action |
|---|---|---|
| G1 triggered a Full GC automatically — space exhaustion or IHOP too high | Lower IHOP; check for memory leak; increase heap | |
| Application code called | Find the call site; suppress with | |
| Old gen occupancy not dropping after Full GC | Memory leak — objects held by long-lived references | Take heap dump; analyse with Eclipse MAT |
| Young GC frequency increasing over time | Allocation rate increasing, or Eden shrinking due to G1 adapting to pause goal | Check |
| Allocation outpacing concurrent GC | Increase heap; reduce allocation rate | |
| Pause times intermittently much higher than goal | Safepoint delays — threads slow to reach safepoint (JNI, OSR loops) | Enable safepoint logging; look for long-running native or JNI calls |
| GC overhead limit exceeded (OOM) | JVM spending >98% of time in GC with <2% heap freed | Memory leak or heap too small — double heap or fix leak |