JVM garbage collection tuning has a folklore problem. Teams copy flag combinations from posts written years ago for different workloads on different hardware, observe no improvement or a regression, and conclude the JVM is inscrutable. Meanwhile the actual lever — which collector is running and how much heap it has — went untouched.
Start there, and most tuning becomes unnecessary.
What changed in 2026
- Low-pause collectors matured further. Concurrent collectors delivering consistently short pauses at large heap sizes became viable for a broader range of applications.
- Defaults improved. Modern JVM defaults became genuinely good for typical workloads, which reduced how much tuning most applications need.
- Container awareness became reliable. Correct detection of container memory limits removed a long-standing source of surprising out-of-memory failures.
- Allocation reduction got emphasized. Guidance shifted toward generating less garbage rather than collecting it faster.
Choosing a collector
| Priority |
Collector characteristics to seek |
| Throughput on batch work |
Parallel collection, longer pauses acceptable |
| Balanced server workload |
Generational concurrent collection, moderate pauses |
| Low and predictable latency |
Concurrent low-pause collector, some throughput cost |
| Small heap, short-lived process |
Simple serial collection; lowest overhead |
| Very large heap with latency requirements |
Concurrent collector designed for large heaps |
The choice follows from what you are optimizing. A batch job that runs for an hour cares about total throughput and does not care about a pause. A request-serving service cares enormously about the tail of the pause distribution and can afford some throughput loss to shorten it.
Getting this right matters more than any individual flag, and it is frequently the only change needed.
What to measure
Average pause time is a nearly useless metric. If most collections pause for a few milliseconds and occasional ones pause for a second, the average looks fine and users experience the second.
Measure the distribution: median, high percentiles, and the maximum. The high percentiles are what correlate with the latency your users see, and they are what a low-pause collector improves.
Also measure allocation rate and collection frequency. A high allocation rate means the collector runs constantly regardless of how efficient it is, and reducing garbage generation addresses the cause rather than the symptom. Object pooling, avoiding unnecessary intermediate collections, and reusing buffers in hot paths are frequently more effective than any collector tuning.
Enable garbage collection logging in production. It is low overhead and it is the only way to know what is actually happening rather than inferring from symptoms.
Heap sizing
Too small and the collector runs constantly, spending a large fraction of CPU time on collection. Too large and, on collectors whose pause scales with heap, individual pauses get long — and you may be reserving memory the application never needs.
The practical approach is to observe live-set size after a full collection under representative load, then size the heap at a comfortable multiple of that. Enough headroom that collections are not constant, not so much that pauses suffer.
In containers, ensure the JVM sees the container limit rather than the host's memory. Modern versions handle this correctly by default, and a heap sized against host memory inside a constrained container is a reliable path to termination by the orchestrator.
The general profiling approach for finding what allocates is in reading flame graphs, and allocation profiles read the same way.
Common mistakes
- Copying flag sets from posts. Tuned for a different workload.
- Tuning before choosing the collector. The dominant decision, frequently skipped.
- Optimizing average pause. The tail is what users feel.
- Ignoring allocation rate. Generating less garbage beats collecting faster.
- No GC logging in production. Diagnosis by guesswork.
- Heap sized against host memory in a container. Terminated by the orchestrator.
FAQ
Should I tune at all?
Modern defaults are good. Start by choosing the right collector for your latency profile and sizing the heap sensibly, then measure before changing anything else.
How do I reduce allocation?
Profile allocations to find the hot paths, then reduce intermediate object creation in those specific places. Broad avoidance of allocation makes code worse for no benefit.
Does a bigger heap always mean fewer pauses?
Fewer collections, and on some collectors longer individual pauses. Concurrent collectors designed for large heaps break that relationship, which is why collector choice comes first.
What about memory leaks?
Tuning does not fix a leak. Growth that survives full collection is retention — the equivalent procedure for another runtime is in finding memory leaks in Node.
Where to go next
For allocation and CPU analysis, read reading flame graphs. For a different runtime's leak hunting, finding memory leaks in Node, and for connection resource tuning, connection timeout tuning.