Contents

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.

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.

G1GC does not compact the heap during normal Mixed GC — it evacuates live objects to new regions. This means fragmentation can occur if many regions contain a mix of live and dead objects. G1 handles this via the region selection algorithm, but very high allocation rates can overwhelm it.

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.

# Production G1GC configuration — 8 GB API server java -XX:+UseG1GC \ -Xms8g -Xmx8g \ -XX:MaxGCPauseMillis=100 \ -XX:InitiatingHeapOccupancyPercent=35 \ -XX:G1HeapRegionSize=8m \ -XX:G1NewSizePercent=20 \ -XX:G1MaxNewSizePercent=40 \ -XX:+UnlockDiagnosticVMOptions \ -Xlog:gc*,gc+humongous=debug:file=/var/log/gc.log:time,uptime,tags:filecount=5,filesize=20m \ -jar app.jar

Reading the G1GC log — key lines to watch:

# Young GC — normal, fast [1.234s] GC(5) Pause Young (Normal) (G1 Evacuation Pause) 512M->320M(8192M) 8.234ms # Mixed GC — old-gen collection starting [60.1s] GC(42) Pause Young (Mixed) (G1 Evacuation Pause) 3200M->2800M(8192M) 42.1ms # Concurrent marking triggered [58.7s] GC(40) Concurrent Mark Cycle [58.7s] GC(40) Pause Remark 3100M->3100M(8192M) 5.2ms ← STW remark # ⚠ To Mark (To-Space Exhaustion) — G1 ran out of space during evacuation [120s] GC(89) To-space exhausted ← precursor to Full GC # ⚠ Full GC — G1 failed to keep up [120.5s] GC(90) Pause Full (G1 Compaction Pause) 7900M->4200M(8192M) 3450ms

Tuning actions based on log signals:

Log signalCauseFix
Full GC (Ergonomics)IHOP too high — marking starts too lateLower -XX:InitiatingHeapOccupancyPercent (35 → 25)
Full GC (Humongous)Short-lived large objects allocated in Old GenIncrease -XX:G1HeapRegionSize so fewer objects qualify as humongous
To-space exhaustedAllocation rate exceeds evacuation rateIncrease heap size; reduce -XX:MaxGCPauseMillis to allow larger collections
Young GC > pause goalYoung Gen too largeLower -XX:G1MaxNewSizePercent
Old Gen filling despite GCPromotion rate high — objects not dying youngCheck 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.

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 — low-latency service (Java 21 with generational ZGC) java -XX:+UseZGC \ -XX:+ZGenerational \ -Xms16g -Xmx16g \ -XX:SoftMaxHeapSize=14g \ -XX:+UnlockDiagnosticVMOptions \ -Xlog:gc*:file=/var/log/gc.log:time,uptime,tags:filecount=5,filesize=20m \ -jar app.jar

ZGC log — key lines:

# Short STW pauses (all under 1 ms in healthy systems) [1.100s] GC(0) Pause Mark Start (Major) 0.071ms [1.500s] GC(0) Pause Mark End (Major) 0.234ms [1.700s] GC(0) Pause Relocate Start (Major) 0.108ms # Allocation stall — application cannot allocate; GC can't free fast enough [120s] GC(45) Allocation Stall (MyThread) 15.234ms ← bad sign # Heap usage after collection [1.710s] GC(0) Heap: 12800M(16384M)->8192M(16384M)

ZGC tuning rules:

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.

# Shenandoah — Red Hat / Corretto JDK java -XX:+UseShenandoahGC \ -XX:ShenandoahGCHeuristics=adaptive \ -Xms8g -Xmx8g \ -Xlog:gc*:file=/var/log/gc.log:time,uptime,tags \ -jar app.jar # Compact heuristic — aggressive GC for memory-constrained environments java -XX:+UseShenandoahGC \ -XX:ShenandoahGCHeuristics=compact \ -Xms2g -Xmx4g \ -jar app.jar

A side-by-side view of all production-grade collectors to help with selection decisions:

FeatureSerial / ParallelG1GCZGCShenandoah
Available sinceJava 1 / Java 1.3Java 7 (default Java 9)Java 11 (prod Java 15)Java 12
Max pauseSeconds (STW all phases)10–200 ms (tunable)<1 ms1–5 ms
ThroughputHighest (no concurrent overhead)HighModerate (load barriers)Moderate
Heap scaleSmall–medium4–32 GB optimalUp to terabytes4–64 GB
GC threadsParallel, STW onlyParallel STW + concurrentMostly concurrentMostly concurrent
GenerationalYesYesYes (Java 21+)Partial (in progress)
JDK availabilityAll JDKsAll JDKsOpenJDK, GraalVMOpenJDK, Corretto, Red Hat
Best forBatch / single-core containersGeneral-purpose APIsUltra-low latency, huge heapsLow latency on Red Hat JDKs

There is no universally best GC. The decision tree below covers the most common production scenarios:

Always validate GC choice with load tests in a staging environment before changing the GC in production. A GC that reduces p99 latency may lower overall throughput — measure both before committing.

A short reference to the most important patterns in unified GC logs (-Xlog:gc*). Once you can recognise these by eye, GC tuning becomes much faster.

PatternMeaningAction
Pause Full (Ergonomics)G1 triggered a Full GC automatically — space exhaustion or IHOP too highLower IHOP; check for memory leak; increase heap
Pause Full (System.gc())Application code called System.gc()Find the call site; suppress with -XX:+DisableExplicitGC
Old gen occupancy not dropping after Full GCMemory leak — objects held by long-lived referencesTake heap dump; analyse with Eclipse MAT
Young GC frequency increasing over timeAllocation rate increasing, or Eden shrinking due to G1 adapting to pause goalCheck -XX:G1MaxNewSizePercent; profile allocation
Allocation Stall (ZGC)Allocation outpacing concurrent GCIncrease heap; reduce allocation rate
Pause times intermittently much higher than goalSafepoint 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 freedMemory leak or heap too small — double heap or fix leak