Diagnose from a jstat readout why a service spends most of its time in GC
A Java service becomes unresponsive under ordinary load. A restart helps for a few hours, then the symptom returns. The heap is -Xmx4g and no application configuration changed before it appeared. jstat -gcutil <pid> 10s prints, over five minutes:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 12.44 38.10 97.82 95.1 91.3 412 6.905 18 41.230 48.135
0.00 9.87 44.02 98.61 95.1 91.3 414 6.941 26 61.884 68.825
0.00 11.03 51.77 99.04 95.1 91.3 415 6.955 34 82.117 89.072
0.00 10.15 60.31 99.31 95.1 91.3 416 6.960 43 104.556 111.516
Columns: E eden, O old gen, M metaspace (all % used); FGC/FGCT full-GC count and seconds.
Diagnose the cause, and name the tools you would use to confirm it.
Old-gen occupancy O stays above 97% and never drops after a full GC, while FGC and FGCT climb steeply — back-to-back full collections that reclaim almost nothing. That is the signature of a retained live set (a leak), not of an undersized young generation. Confirm with a heap dump (jmap -dump) read in Eclipse MAT or VisualVM, whose dominator tree names the retaining GC root, plus -Xlog:gc* or JFR for the pause history.
- ✗Reading a high old-gen occupancy as healthy rather than as a retained live set
- ✗Tuning heap flags before confirming what is retained, with a heap dump
- ✗Assuming a rising full-GC count is normal for any long-running service
- →What does a dominator tree in Eclipse MAT tell you that
jstatcannot? - →How would the same readout look if the young generation really were too small?
Analysis
What the readout says.
O(old gen) — 97.8 → 99.3%: occupancy does not drop after the full collections. They are reclaiming nothing.FGC18 → 43 in five minutes,FGCT41 → 104 s: the service spent more than a minute out of five inside full GCs. That is the unresponsiveness — GC overhead.E(eden) rises and resets — the young generation is healthy; minor collections stay cheap (YGCTbarely moves).M(metaspace) is flat — class loaders are not involved.
Conclusion. The live set no longer fits in the old generation and keeps growing: something retains objects through strong references. The classics are an unbounded cache, a static collection, an unremoved listener, a ThreadLocal in a pooled thread.
How to confirm.
jcmd <pid> GC.heap_dump /tmp/heap.hprof # heap snapshot (or jmap -dump:live,format=b,file=…)
jcmd <pid> GC.class_histogram # quick histogram: which classes hold the bytes
Then Eclipse MAT (or VisualVM): the dominator tree and Leak Suspects report show not "what is big" but what retains it — the chain from a GC root down to the object. Pause history comes from -Xlog:gc*:file=gc.log and Java Flight Recorder.
The correct order is: find the retaining root first, and only then touch a GC flag.