Skip to content

GC tuning

  • performance

The honest answer to “how do I tune the GC?” on a modern JDK is: pick the right one, then mostly leave it alone. The default (G1) is well-tuned; reaching for flags before reading the logs is how you make things worse.

A modern JDK ships five collectors, but only three are a real choice for a server workload. Choose by pause-time tolerance, not gut feel:

GC Default? Pause time Heap range Use when…
G1 yes ~10–200 ms 4 GB – ~64 GB Default. Balanced. Don’t switch without a reason.
ZGC no sub-ms (<1 ms) ~8 GB – multi-TB Pause-sensitive services (APIs, gateways), large heaps.
Shenandoah no ~1–10 ms similar to ZGC Same niche as ZGC; OpenJDK distros that ship it.
Parallel no seconds (stop-the-world) small heaps Batch/throughput jobs where pauses don’t matter.
Serial no seconds tiny CLI tools, tiny containers. Picked automatically — see below.
Terminal window
# G1 (default — no flag needed)
java -XX:+UseG1GC -jar app.jar
# ZGC
java -XX:+UseZGC -jar app.jar
# Shenandoah
java -XX:+UseShenandoahGC -jar app.jar

Heuristic. If p99 latency matters and you have ≥4 GB heap, try ZGC. Otherwise stay on G1.

Bigger sin than the wrong GC: wrong heap sizing.

Terminal window
# Pin min == max. JVM won't waste cycles growing the heap; behavior is predictable.
-Xms4g -Xmx4g

In containers, prefer the percentage flags so the JVM follows the cgroup memory limit instead of the host’s:

Terminal window
-XX:InitialRAMPercentage=70 -XX:MaxRAMPercentage=70

Leave ~25–30% of the container memory for non-heap (metaspace, direct buffers, native code, the GC itself). A JVM OOMKilled by the kernel rarely leaves a useful heap dump.

Enable unified GC logging — the -Xlog framework landed in JDK 9 and the format has been stable since:

Terminal window
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=20M

What to look for:

  • Pause time (Pause Young (Normal), Pause Full) — the actual stop-the-world duration. This is the number that hurts.
  • Allocation rate — climbing rate over hours means a leak or a workload shift; not a tuning problem.
  • “to-space exhausted” / “Allocation Failure” under G1 — heap too small or bursty allocation; raise -Xmx before fiddling with regions.
  • Full GC on G1 or ZGC — should be never in steady state. Seeing one means the heap can’t keep up, or you ran System.gc(). Add -XX:+DisableExplicitGC to rule out the latter.

Free, faster than eyeballing logs: drop gc.log into GCeasy or use jdk.jfr flight recordings:

Terminal window
# 60-second recording, ~1% overhead on the default profile. With both duration= and
# filename= the recording writes itself out when it ends — no dump step needed.
jcmd <pid> JFR.start name=gc duration=60s filename=app.jfr

To grab a snapshot early, or when you started an open-ended recording, dump it by name:

Terminal window
jcmd <pid> JFR.start name=gc # runs until stopped
jcmd <pid> JFR.dump name=gc filename=app.jfr
jcmd <pid> JFR.stop name=gc

Open the .jfr in JDK Mission Control.

Common knobs (only after the logs justify them)

Section titled “Common knobs (only after the logs justify them)”
Terminal window
# G1: target max pause (default 200 ms). Lower = more frequent, smaller GCs.
-XX:MaxGCPauseMillis=100
# G1: region size. Default auto-picks based on heap; only override if you have
# huge object allocations triggering humongous-object regions.
-XX:G1HeapRegionSize=16m
# ZGC: cap concurrent GC threads on small containers.
-XX:ConcGCThreads=2

Resist the temptation to copy a flag list from a 2017 blog post. Most of those flags either no-op on modern JDKs or pessimize the default heuristics.

These are application problems, not GC problems:

  • Allocation rate too high — profile and reduce churn (object pooling, primitive arrays, avoiding boxing in hot paths).
  • Long-lived objects that survive into old gen and force mixed/old collections — usually a cache without bounds.
  • Memory leakFull GC happens, frees little, repeats. Take a heap dump (Thread dump covers the sibling diagnostic) and find the retained set in MAT or JDK Mission Control.