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.

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