Skip to content

Lesson 2 of 9 · 25m

Memory, Garbage Collection, and Reading a Running JVM

Heap layout, metaspace, jcmd/jstat/JFR, and the dump-you-take-before-you-need-it diagnostic loop.

A production box starts paging at 3 a.m.: response times creep up, recover, then creep up again. The pattern is almost always memory — but "it's using too much" tells you nothing until you can see which heap, which generation, and which objects. This lesson is the diagnostic loop you run before guessing.

How the heap is actually laid out

The Java heap splits into a young generation (eden plus two survivor spaces) and an old generation. Objects are born in eden; survivors of a few minor GCs get promoted to old. G1 — the default collector since Java 9 — divides the heap into regions rather than fixed generations, but the young/old logic still holds. Class metadata lives in metaspace, which replaced PermGen in Java 8 and grows from native memory — a subtle leak source we'll come back to.

Check a live layout:

jcmd <pid> GC.heap_info
jcmd <pid> VM.flags
jstat -gc <pid> 1s

jstat -gc prints columns like S0U, EU, OU — survivor, eden, old utilization. Watch OU across GC cycles: if it never recovers to a baseline, something is being retained.

Get the dump before you need it

When it's really leaking, take a heap dump and a histogram:

jcmd <pid> GC.heap_dump /var/tmp/app.hprof
jmap -histo:live <pid> | head -30

GC.heap_dump writes the whole heap (size the disk accordingly — and treat the file as sensitive: it contains every string your app held, including credentials). Open it in Eclipse MAT; the dominator tree answers "who holds all this" in one click.

For lower overhead, Java Flight Recorder is built into the JDK:

jcmd <pid> JFR.start duration=60s filename=/var/tmp/app.jfr

The bug that ships: metaspace leaks

Hot redeploys, script engines, JSP compilation, and bytecode generators (CGLIB proxies — which Spring uses) all create classes. Every class lives in a classloader; a retained classloader pins its classes forever in metaspace. The symptom is OutOfMemoryError: Metaspace without heap pressure. The fix is finding who holds the old classloader — the heap dump shows it, because classloaders are objects too.

Try this

Write a loop that fills a static List<byte[]> with 1 MB arrays, run it with -Xmx256m, and watch jstat -gc until OU saturates and you get OutOfMemoryError: Java heap space. Then rerun with -XX:+HeapDumpOnOutOfMemoryError and open the dump. Ten minutes, and you've done a real forensic loop.

Field notes

  • In containers, MaxRAMPercentage applies to the cgroup limit — verify with jcmd <pid> VM.flags inside the pod, not on the node.
  • Full GCs that take seconds mean either the heap is too big for your pause budget or you're retaining most of it. G1's target is -XX:MaxGCPauseMillis (default 200 ms).
  • Heap dumps are secrets-bearing artifacts. Restrict file permissions, and never attach one to a ticket or a chat channel.