Garbage Collection
GC roots, Serial, Parallel, G1, ZGC, Shenandoah — when to use which
Garbage Collection is the JVM automatically reclaiming memory occupied by objects that are no longer reachable. Choosing the right GC algorithm is a key production concern — the wrong choice leads to long pauses, unpredictable latency, or poor throughput. Understanding GC roots, generational collection, and each collector's trade-offs is essential for JVM performance tuning.
Key Points
- GC Roots: the starting points for reachability analysis — stack variables, static fields, JNI references, active threads
- An object is garbage when no GC root can reach it (transitively) — GC walks the object graph from roots
- Generational hypothesis: most objects die young — Young Gen (Eden + 2 Survivor spaces) collected frequently with cheap Minor GC
- Minor GC: copies surviving young-gen objects between Survivor spaces; objects surviving N GCs (default 15) are promoted to Old Gen
- Major/Full GC: collects Old Gen — typically slower and potentially Stop-The-World
- Serial GC (-XX:+UseSerialGC): single-threaded, simplest — best for small heaps (<256MB), batch jobs
- Parallel GC (-XX:+UseParallelGC): multi-threaded, throughput-optimised — Java 8 default, long STW pauses
- G1 GC (-XX:+UseG1GC): heap divided into ~2000 equal regions, concurrent + incremental — Java 9+ default, predictable pauses
- ZGC (-XX:+UseZGC): sub-millisecond pauses even on TB heaps, concurrent mark+relocate — Java 15+ production-ready
- Shenandoah (-XX:+UseShenandoahGC): similar to ZGC, concurrent evacuation — Red Hat, OpenJDK
| GC | Pause type | Throughput | Latency | Heap size | Default since |
|---|---|---|---|---|---|
| Serial | STW all phases | Low | High | <256MB | Java 1 |
| Parallel | STW all phases | High | High | Medium | Java 8 |
| G1 | Short concurrent + small STW | Good | Low | 6GB–50GB | Java 9 |
| ZGC | Sub-ms STW (roots only) | Good | Very low | Up to TB | Java 15 |
| Shenandoah | Concurrent evacuation | Good | Very low | Medium–large | Java 12 |
JVM GC tuning flags: G1 with pause target, ZGC for low-latency, GC logging, heap dump on OOM
# Common JVM GC flags
# G1 with 4GB heap, 200ms max pause goal
java -Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar
# ZGC for low-latency services
java -Xmx16g -XX:+UseZGC -XX:SoftMaxHeapSize=12g -jar app.jar
# Enable GC logging (Java 11+)
java -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20m -jar app.jar
# Print GC summary on exit
java -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar app.jar
# Useful diagnostic flags
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps/
-XX:+PrintGCApplicationStoppedTime # show STW pause times
-XX:NewRatio=3 # old:young = 3:1
-XX:SurvivorRatio=8 # eden:survivor = 8:1Real-World Example
A 99th-percentile latency spike every few minutes is often a Full GC. First step: enable GC logging and use GCViewer or GCEasy to visualise pauses. If Old Gen fills gradually, you likely have a memory leak (use heap profiler like JFR or async-profiler). If promotions are too fast (young objects promoted to old), tune -XX:NewSize / -XX:MaxNewSize to give Young Gen more space.