English
JVM Performance Tuning Guide
JVM tuning is not magic — it is a loop: measure → change one thing → verify. This guide gives you the workflow, the symptoms-to-fixes map, and the knobs that matter.
Step 1: Measure before you touch anything
Never tune blind. Enable GC logging (safe in production, < 3% overhead) and analyze a window that covers peak traffic:
bash
# Java 9+
java -Xlog:gc*:file=gc.log:time,uptime -jar app.jar
# Java 8
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar app.jarThe report tells you which of the four common problems you actually have.
Need to keep the log on-premise, or analyze it in batch/CI? Do the same thing locally with GCViewer:
bash
# one row per event — perfect for diffing a tuning run against the previous one
java -jar gcviewer-1.37.jar gc.log before.csv -t CSV_TSStep 2: Match the symptom to the fix
| Symptom in the report | Likely cause | What to try |
|---|---|---|
| Frequent Full GC | old gen too small / leak / oversized cache | Raise -Xmx; check for leaks (after-GC heap climbing); review cache sizes |
| High allocation rate | short-lived object churn | Object pooling or caching hot paths; reduce per-request garbage |
| Max pause > SLO | wrong collector or heap too big for collector | Switch to G1/ZGC; for G1 set -XX:MaxGCPauseMillis |
| Throughput < 90% | GC frequency too high | Bigger young gen (-Xmn / -XX:NewRatio); bigger heap |
| Metaspace GCs | classloader churn | Raise -XX:MaxMetaspaceSize; hunt dynamic proxies |
System.gc() in causes | explicit calls (RMI/NIO) | -XX:+DisableExplicitGC — verify Direct ByteBuffer reliance first |
| Humongous allocations (G1) | objects > half a region | Raise -XX:G1HeapRegionSize; refactor large arrays |
Step 3: Change one knob at a time
Tuning one thing, re-measuring, and comparing reports is the only way to know what worked. The knobs that cover 90% of cases:
| Knob | Effect |
|---|---|
-Xms = -Xmx | Fixed heap size — removes resize pauses, more predictable |
-Xmn / -XX:NewRatio | Young gen size — the main lever on GC frequency |
-XX:+UseG1GC / -XX:+UseZGC | Collector choice — the main lever on pause length |
-XX:MaxGCPauseMillis=200 | G1 pause target (don't set it unrealistically low) |
-XX:MaxMetaspaceSize | Class metadata cap |
-XX:+DisableExplicitGC | Ignore System.gc() calls |
Pick the lever for your collector
The same symptom has a completely different knob per collector. First confirm which one runs in production (java -XX:+PrintFlagsFinal -version | grep Use.*GC):
| Collector | Use case | Key flags | What to watch in the report |
|---|---|---|---|
| G1 (default from JDK 9) | heaps above 4 GB, throughput and latency balanced | -XX:MaxGCPauseMillis=200; humongous objects → -XX:G1HeapRegionSize; late concurrent cycle → -XX:InitiatingHeapOccupancyPercent | max pause, humongous allocations, share of Mixed GCs |
| ZGC | latency first, heaps up to TBs | -XX:+UseZGC; add -XX:+ZGenerational on JDK 21+ | whether pauses stay in the low milliseconds, Allocation Stalls (collection cannot keep up) |
| Shenandoah | low latency, medium heaps | -XX:+UseShenandoahGC | pauses, concurrent cycles being aborted |
| Parallel | throughput first, batch jobs | -XX:+UseParallelGC, -XX:GCTimeRatio=19 (≈95% throughput), -XX:MaxGCPauseMillis | throughput, Full GC count |
| Serial | containers / small heaps (hundreds of MB) | -XX:+UseSerialGC | single pause duration |
| CMS | JDK 8 only (deprecated in 9, removed in 14) | -XX:CMSInitiatingOccupancyFraction=70, -XX:+UseCMSInitiatingOccupancyOnly | remark pauses, Concurrent Mode Failures |
Three copy-paste flag templates
bash
# 1. Online service (G1, 4-core / 8 GB container) — latency and throughput balanced
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/dump/
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=50M
# 2. Batch / offline job (throughput first)
-Xms8g -Xmx8g
-XX:+UseParallelGC
-XX:GCTimeRatio=19 # target 1/(1+19) = 5% of time in GC, i.e. 95% throughput
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime
# 3. Latency-sensitive service (ZGC, JDK 21+)
-Xms8g -Xmx8g
-XX:+UseZGC
-XX:+ZGenerational
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=50M-Xms equal to -Xmx removes the pauses caused by heap resizing; filecount/filesize let you keep logging on forever without filling the disk — just read the rotated files as one continuous log when analyzing.
Step 4: Verify and set a baseline
Re-analyze the new GC log and compare it with the old report: throughput up? max pause down? Full GCs gone? Save both reports (PDF/JSON) as your baseline — after the next release, one analysis tells you whether GC behavior regressed.
Checklist
TIP
New to the terminology? Start with JVM vs JRE vs JDK, then come back.
Related pages
- GCViewer Usage Guide — offline and batch analysis from the command line
- JVM Diagnostic Tools — jmap, jstack, jstat and Arthas by scenario
- JVM GC Metrics to Monitor — the numbers this workflow measures
- Enable GC Logging — the foundation of every analysis