Skip to content

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.jar

Analyze the log with EasyGC →

The 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_TS

Step 2: Match the symptom to the fix ​

Symptom in the reportLikely causeWhat to try
Frequent Full GCold gen too small / leak / oversized cacheRaise -Xmx; check for leaks (after-GC heap climbing); review cache sizes
High allocation rateshort-lived object churnObject pooling or caching hot paths; reduce per-request garbage
Max pause > SLOwrong collector or heap too big for collectorSwitch to G1/ZGC; for G1 set -XX:MaxGCPauseMillis
Throughput < 90%GC frequency too highBigger young gen (-Xmn / -XX:NewRatio); bigger heap
Metaspace GCsclassloader churnRaise -XX:MaxMetaspaceSize; hunt dynamic proxies
System.gc() in causesexplicit calls (RMI/NIO)-XX:+DisableExplicitGC — verify Direct ByteBuffer reliance first
Humongous allocations (G1)objects > half a regionRaise -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:

KnobEffect
-Xms = -XmxFixed heap size — removes resize pauses, more predictable
-Xmn / -XX:NewRatioYoung gen size — the main lever on GC frequency
-XX:+UseG1GC / -XX:+UseZGCCollector choice — the main lever on pause length
-XX:MaxGCPauseMillis=200G1 pause target (don't set it unrealistically low)
-XX:MaxMetaspaceSizeClass metadata cap
-XX:+DisableExplicitGCIgnore 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):

CollectorUse caseKey flagsWhat 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:InitiatingHeapOccupancyPercentmax pause, humongous allocations, share of Mixed GCs
ZGClatency 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)
Shenandoahlow latency, medium heaps-XX:+UseShenandoahGCpauses, concurrent cycles being aborted
Parallelthroughput first, batch jobs-XX:+UseParallelGC, -XX:GCTimeRatio=19 (≈95% throughput), -XX:MaxGCPauseMillisthroughput, Full GC count
Serialcontainers / small heaps (hundreds of MB)-XX:+UseSerialGCsingle pause duration
CMSJDK 8 only (deprecated in 9, removed in 14)-XX:CMSInitiatingOccupancyFraction=70, -XX:+UseCMSInitiatingOccupancyOnlyremark 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.