The 30 second answer
I measure before changing anything: heap used after garbage collection, plus a heap dump and a class histogram to see what's really taking space. Then I fix leaks like unbounded caches, static maps, listeners that are never removed, and thread locals never cleared. I stream large data instead of loading it all, use leaner data structures, and size the heap from measured live data, including container limits.
Measure:
jcmd <pid> GC.class_histogram # which classes use the most memory
jcmd <pid> GC.heap_dump /tmp/app.hprof # analyze in Eclipse MAT or VisualVM
-XX:+HeapDumpOnOutOfMemoryError # capture the moment it failsLook at retained size and the path to GC roots of the biggest objects.
Common fixes:
- Leaks: caches without size or TTL limits, static collections, unremoved listeners,
ThreadLocalvalues in pooled threads withoutremove(). - Large data: stream from the database or files (pagination, cursors) instead of loading everything with
findAll(). - Overhead: boxed collections like
List<Integer>versusint[], oversized initial capacities, duplicate strings (G1 supports-XX:+UseStringDeduplication). - Sizing: set
-Xmxfrom live data after a full GC, plus headroom.
[Opinion] In containers prefer -XX:MaxRAMPercentage (for example 70 to 75) over a fixed -Xmx, and leave room for native memory: thread stacks, Metaspace, direct buffers and the code cache.
Sources: jcmd (GC.heap_dump, GC.class_histogram), Troubleshooting Guide: Memory Leaks, java command options