GC Tuning Before Your Load Test: A Checklist

Running a load test without tuning GC is like measuring car speed with flat tires. G1GC settings that will stop your test from being about garbage collection.

24 марта 2025 г.9 мин чтения
JVMG1GCJavaHeap

Why GC Tuning Matters Before Load Testing

You're testing application performance, not JVM performance. If your load test results look like this:

`` P99: 8,200ms P50: 45ms `

...the bimodal distribution (fast most of the time, occasional 8-second spikes) is almost always GC pauses — especially Stop-The-World Full GC events. You're not measuring your application, you're measuring garbage collection.

Fix GC first, then run the load test.

Step 1: Use G1GC (If Not Already)

G1GC has been the default since Java 9. If you're on Java 8 with CMS or ParallelGC, switch:

`bash -XX:+UseG1GC `

G1GC is designed for low latency. It does incremental collection instead of full heap sweeps, which eliminates the multi-second STW pauses of older collectors.

Step 2: Set Heap Size Correctly

The most common mistake: not setting explicit heap bounds.

`bash # Wrong — JVM uses defaults, heap grows and shrinks during test java -jar app.jar

# Right — fixed heap, no resize overhead, predictable behavior java -Xms4g -Xmx4g -jar app.jar `

Set -Xms == -Xmx. This prevents heap resizing during the test (which causes pauses and skews results).

How big? Rule of thumb: set heap to 2-3× the expected live data size at peak load. Check with jstat after warmup:

`bash jstat -gcutil 1000 10 # Look at "O" (Old Gen %) after warmup — that's your baseline `

Step 3: Tune G1GC Pause Target

G1GC tries to meet a pause time target. The default is 200ms — fine for throughput, but for latency-sensitive APIs you want lower:

`bash -XX:MaxGCPauseMillis=50 `

Setting this too low forces G1GC to collect more frequently in smaller batches, which reduces pause duration but increases GC CPU overhead. 50ms is a good starting point for APIs with P99 < 500ms targets.

Step 4: Enable GC Logging

You can't tune what you can't see. Enable GC logs for every load test:

`bash -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20m `

This writes structured GC logs with timestamps. After the test, check:

  • Were there any Full GC events? (Should be zero in a healthy run)
  • What was the max pause duration?
  • How often was GC running?

Step 5: Warmup Period

JVM performance improves significantly after JIT compilation warms up. A cold JVM runs interpreted bytecode — 10-100× slower than JIT-compiled code.

Your load test must include a warmup phase:

`javascript // k6 warmup stages stages: [ { duration: '3m', target: 50 }, // warmup — not measured { duration: '10m', target: 200 }, // measured baseline { duration: '5m', target: 500 }, // stress phase { duration: '2m', target: 0 }, // cooldown ] `

In JMeter: use a Ramp-Up period of at least 2-3 minutes before starting data collection. Never report results from the first 2 minutes.

Step 6: Pre-Allocate String Pool (Optional)

For string-heavy applications, increasing the string pool can reduce GC pressure:

`bash -XX:StringTableSize=1000003 # prime number, ~1M buckets `

Only worth doing if heap dump analysis shows many duplicate String objects.

The Full JVM Flags Checklist

`bash java \ # Heap: fixed size, no resize -Xms8g -Xmx8g \ # GC: G1 with 50ms target -XX:+UseG1GC \ -XX:MaxGCPauseMillis=50 \ # GC logging: always on -Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=20m \ # Heap dump on OOM (for debugging, not production) -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/var/log/heap-dump.hprof \ # JIT: print compilation (optional, for deep analysis) # -XX:+PrintCompilation \ -jar app.jar `

Verifying GC Health After the Test

`bash # Parse GC log for Full GC events (should be 0) grep "Pause Full" gc.log | wc -l

# Find the longest pause grep "Pause Young" gc.log | awk '{print $NF}' | sort -n | tail -5

# GC overhead (should be < 5% of wall time) grep "GC overhead" gc.log ``

If you see Full GC events, your heap is too small OR you have a memory leak. Fix that before interpreting any latency numbers.

Common Mistakes

Mistake 1: Testing with default heap JVM starts with 256MB and grows. Each resize triggers a pause. Your P99 spikes are heap resizes.

Mistake 2: Not waiting for JIT warmup First 60 seconds of a load test are always slower. Never include warmup in your reported numbers.

Mistake 3: Ignoring GC logs "The test passed, P99 was 120ms" — but was that after excluding the 8-second GC pause that hit 0.1% of requests?

Mistake 4: Running GC tuning experiments during load test Change one thing at a time. Each load test run should have consistent JVM flags.

Хочешь попрактиковаться?
Примени знания в интерактивном кейсе
Смотреть кейсы →← Все статьи