GC Algorithms & Tuning
GC Algorithms & Tuning
Understanding which garbage collector is running and why it behaves the way it does is the difference between guessing at JVM flags and making deliberate, measurable decisions. This lesson focuses on two collectors you will encounter in modern production services — G1GC and ZGC — and the baseline heap-sizing flags that govern their behaviour.
Why Different GC Algorithms Exist
Every collector is an engineering trade-off across three axes:
- Throughput — total application work done per unit of time (GC pauses subtract from this).
- Latency — the worst-case pause a single user request can experience.
- Footprint — how much memory the collector itself consumes for its data structures.
No collector wins on all three simultaneously. Choosing the right one starts with knowing your SLO: is a 99th-percentile latency target more important than raw throughput?
Garbage-First (G1GC)
G1 has been the default collector since JDK 9. Its defining idea is that it divides the entire heap into a large number of equal-sized regions (typically 1 MB–32 MB each, chosen automatically). Regions are not permanently assigned to young or old generations; the collector dynamically reclassifies them.
How G1 Works at a High Level
- Young-only phase — G1 runs concurrent marking while the application runs. Short stop-the-world (STW) minor GC pauses evacuate live objects from young regions into survivor or old regions.
- Mixed collection phase — after marking identifies which old regions are mostly garbage, G1 includes a subset of those old regions in subsequent collections ("mixed GC"). It collects the regions with the most garbage first — hence the name Garbage-First.
- Full GC — a last-resort single-threaded (pre-JDK 10) or parallel (JDK 10+) full compacting collection. You want to avoid this in production.
-XX:MaxGCPauseMillis, default 200 ms) by adjusting how many regions it collects per cycle. It does not guarantee the target — it is a hint, not a hard deadline.
Key G1 Flags
A real production command line might look like:
ZGC — Ultra-Low-Latency Garbage Collection
ZGC (production-ready since JDK 15, generational ZGC added in JDK 21) is designed for one primary goal: sub-millisecond STW pauses regardless of heap size. It achieves this through two advanced techniques:
- Colored pointers — ZGC encodes GC metadata (mark bits, relocation state) directly inside the 64-bit object reference. This lets it do load-barrier work at the point where the reference is used rather than scanning the whole heap while paused.
- Concurrent relocation — unlike G1 (which must move objects STW), ZGC relocates live objects while the application is running using a self-healing load barrier that transparently redirects stale references.
-XX:+ZGenerational (or use -XX:+UseZGC -XX:+ZGenerational on JDK 21).
Key ZGC Flags
G1 vs ZGC: When to Choose Which
| Concern | G1GC | ZGC |
|---|---|---|
| Pause target | Tens to hundreds of ms | Sub-millisecond |
| Throughput overhead | Low (~5–10% vs. Parallel GC) | Slightly higher (load barriers) |
| Heap size sweet spot | 4 GB – 32 GB | Any, including multi-terabyte |
| Memory footprint | Lower | Slightly higher (colored pointer metadata) |
| Minimum JDK | JDK 9 | JDK 15 (production); JDK 21 for generational |
| Typical use case | Web services, batch, microservices | Real-time trading, gaming, latency-critical APIs |
Heap-Sizing Flags: The Foundation of GC Tuning
Before touching any collector-specific flag, set the heap size correctly. These three flags apply to every collector:
-Xmx higher than the available physical RAM minus OS and other process overhead. If the JVM must swap to disk, every GC cycle becomes catastrophically slow. A common rule: leave at least 1–2 GB for the OS and other JVM overhead (threads, JIT buffers, Metaspace, direct buffers) when sizing -Xmx.
Enabling GC Logging (Essential for Tuning)
You cannot tune what you cannot measure. Always enable GC logging in production:
This writes rolling GC logs with timestamps. Feed them into GCEasy (gceasy.io) or GCViewer for visual analysis — you will immediately see pause time distributions, allocation rates, and whether you are triggering Full GCs.
A Practical Starting Configuration
Measure pause times from the GC log after running under realistic load before changing anything else. Tune one flag at a time and re-measure — GC tuning is an empirical discipline, not a checklist.
Summary
- G1GC: region-based, pause-time-goal-driven, default since JDK 9, excellent for 4–32 GB heaps.
- ZGC: concurrent relocation with colored pointers, sub-millisecond pauses, generational mode in JDK 21 greatly improves throughput.
- Heap sizing: set
-Xms==-Xmxin production, leave headroom for the OS, cap Metaspace. - Always log GC output and analyse it before tuning flags — measure first, change second.