Situation: Production Java service shows steady heap growth over weeks with intermittent OOMs. Task: systematically find root cause with JVM tooling, apply short-term mitigations, and propose long-term prevention.Plan — investigative steps (priority order)1. Capture evidence and stabilize:- Enable GC logging if not present (use -Xlog:gc*:file=/var/log/gc.log:time,level,tags).- Increase monitoring resolution (heap, GC pause, threads, process RSS).- Take immediate heap dump on OOM or with jmap: bash
# safe way to request a heap dump
jcmd <pid> GC.heap_dump /tmp/heap-<ts>.hprof
# or with jmap (may pause):
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
2. Analyze GC logs:- Look for rising tenured/old generation usage between full GCs, promotion failures, frequent full GCs, long GC pause times, fragmentation (-> allocation failures).- Key signals: sustained increase in used-heap after full-GC, increasing survivor/tenured occupancy, CMS/OldGen fragmentation warnings.3. Inspect heap dump (Eclipse MAT / VisualVM / YourKit):- Open heap dump in MAT. Run “Histogram” to list largest classes by retained size.- Use “Top Consumers” / dominator tree to find objects with largest retained sizes.- Use “Path to GC Roots (exclude weak refs)” to find retained paths to suspect objects — look for long-lived collections, caches, threadlocals, ClassLoaders, or singletons holding references.- Common suspicious classes: ArrayList/HashMap with large size, byte[] (large payloads), ConcurrentHashMap, ThreadLocalMap entries, custom cache classes, or frameworks (ehcache, guava cache misconfigured).4. Thread stack traces (jstack) correlation:bash
jstack -l <pid> > /tmp/threaddump-<ts>.txt
- Correlate threads doing heavy allocation (e.g., repeated read into byte[]), blocked threads with stack traces interacting with caches or collecting objects.- Use async-profiler to record allocation/CPU flamegraphs (safe in production for short runs):bash
# run allocation profiler for 30s
./profiler.sh -e alloc -d 30 -f /tmp/alloc.svg <pid>
# CPU hot paths
./profiler.sh -e cpu -d 30 -f /tmp/cpu.svg <pid>
- Flamegraphs show hotspots of allocation; if a small method is responsible for most allocations, inspect code path.5. Identify patterns:- Leaking collections: large Map with keys never removed.- Native/DirectByteBuffer leaks: check for java.nio.DirectByteBuffer instances and their cleaner refs.- ClassLoader leaks (if redeploys): many classes or interned strings tied to a ClassLoader.- Retained-by ThreadLocal: find Thread -> ThreadLocalMap -> value chains.Short-term mitigations (while fixing root cause)- Restart affected process(s) in low-traffic window to free heap (temporary relief).- Add OOM dumps and automated heap-dump-on-oom (-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp).- Tune GC/heap: increase heap if safe, trigger more frequent full GCs to reclaim; for immediate pressure use -XX:MaxGCPauseMillis or choose G1 tuning to reduce fragmentation.- Apply rate-limiting or drop nonessential workloads to slow growth.- Disable features suspected to leak (e.g., certain caches) via feature flags.Root cause fix and verification- Fix code paths identified: clear caches, weaken references (use Weak/SoftReference appropriately), remove ThreadLocal usage or clean up in finally blocks, fix faulty buffer pooling.- Add unit/integration tests that simulate long-running scenarios and validate memory stability (heap-snapshot comparisons).- Deploy patch to staging, run extended load tests with profiler + periodic heap dumps, and verify no growth in dominator tree.Long-term practices to prevent recurrence- Continuous monitoring: heap usage, GC pause metrics, allocation rate, object churn; integrate alerts for sustained growth or survivor/tenured rise.- Regular automated heap-dump sampling in staging under load; regression tests that compare retained sizes.- Adopt allocation profiling in CI for risky changes or memory-sensitive modules.- Use proper caching patterns (bounded caches with eviction, time-based expiry, or use Caffeine/Guava with size limits), and avoid unbounded collections.- Code practices: avoid heavy use of ThreadLocal, clear resources in try-with-resources, document ownership of long-lived references.- Use memory-conscious data structures (primitive arrays, ByteBuffer pooling carefully) and limit retention of large objects.- Postmortem and blameless learning loop: record root cause, mitigate steps, and update runbooks.Why this approach- Combines signal (GC logs, metrics) and evidence (heap dumps, flamegraphs) to find both who allocates and who retains. jstack/async-profiler show allocation origins; heap dump + MAT shows retention paths. Short-term mitigations buy time; long-term practices stop recurrence.