DEV Community

Cover image for KiokuGraph - Heap Dump Analyzer for JVM, .NET, Android and Kotlin
KiokuGraph
KiokuGraph

Posted on

KiokuGraph - Heap Dump Analyzer for JVM, .NET, Android and Kotlin

Reading an OutOfMemoryError: what the stack trace does and does not tell you

At 02:14 your service stops serving and the log says this:

java.lang.OutOfMemoryError: Java heap space
    at java.base/java.util.Arrays.copyOf(Arrays.java:3512)
    at java.base/java.lang.StringBuilder.append(StringBuilder.java:173)
    at com.acme.report.CsvWriter.writeRow(CsvWriter.java:88)
    at com.acme.report.ReportJob.run(ReportJob.java:141)
Enter fullscreen mode Exit fullscreen mode

The instinct is to open CsvWriter.java:88 and start optimising the string handling.

That is almost always the wrong file.



What the stack trace actually says

The trace shows which allocation failed, not which code caused the heap to be full. Those are
different questions with usually different answers.

Think of it as a room filling with water. The stack trace names the last drop that made it overflow.
It says nothing about the tap that has been running for six hours.

CsvWriter asked for a slightly larger char[]. There was no room. CsvWriter is the victim - it
is simply the code unlucky enough to allocate at the moment the heap ran out. The real cause is
whatever is holding memory it should have released.

This is why the trace is often boringly generic. Arrays.copyOf, HashMap.resize,
ArrayList.grow - these appear constantly in OOM traces not because collections are the problem,
but because collections allocate frequently, so they are statistically likely to be holding the
glass when it overflows.

The useful signal in the trace is small: it tells you which thread died, which sometimes tells
you what the application was doing. Nothing more.


Read the message, not the trace

The first line is more informative than everything under it, because the JVM uses distinct messages
for genuinely distinct failures:

Message What it means Where to look
Java heap space The heap is full and GC cannot reclaim enough. A leak, or a heap too small for the workload. This is the common one.
GC overhead limit exceeded Over 98% of time in GC, recovering under 2%. Same causes as above, caught earlier. The JVM is thrashing.
Metaspace Class metadata space exhausted. Classloader leak. Look at redeploys, dynamic proxies, scripting engines - not your data.
Requested array size exceeds VM limit A single array over ~2^31 elements. A bug: an unbounded read, a corrupt length field. Not a leak.
unable to create native thread The OS refused a thread. Thread leak or an OS limit. The heap may be fine.
Direct buffer memory Off-heap ByteBuffer exhausted. NIO or Netty buffers not being released. A bigger heap will not help.

Two of these - Metaspace and Direct buffer memory - are frequently "fixed" by raising -Xmx,
which cannot possibly help because neither lives in the heap. If you take one thing from this
article, take that.


Leak or undersized?

Both produce Java heap space. They need opposite fixes, and you cannot tell them apart from the
trace. You need the shape of memory over time.

Enable GC logging - it is cheap enough for production:

-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20M
Enter fullscreen mode Exit fullscreen mode

Then look at heap used after each full GC, not peak usage. Peak is noise; the post-collection
floor is the truth.

  • Sawtooth that returns to the same floor → not a leak. The workload needs more room than it has, or allocation is too aggressive.
  • Floor climbing steadily over hours or days → a leak. Something retains objects across collections, and no heap size will save you. It buys time proportional to the size increase, and then fails again.

A leak that takes a week to kill a 4 GB heap will take two weeks to kill an 8 GB one. Doubling
-Xmx is a scheduling decision, not a fix.


Get the dump

The trace cannot tell you what is retained. Only the heap can, so capture it:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/dumps
Enter fullscreen mode Exit fullscreen mode

This writes an .hprof at the moment of failure, costs nothing until it triggers, and should be on
in every production JVM. Without it, an OOM at 02:14 leaves you with a stack trace naming an
innocent bystander.

To capture from a running JVM before it dies:

jcmd <pid> GC.heap_dump /var/dumps/app.hprof
Enter fullscreen mode Exit fullscreen mode

Two warnings. It pauses the JVM for roughly a second per gigabyte - do it on one instance out of the
pool. And the file is the size of your live set, so check disk first; a dump that fills the volume
turns one incident into two.


Then ask the only question that matters

With a dump open, ignore what is numerous and find what is retained.

Those differ, and the distinction is the whole game:

  • Shallow size - the bytes of one object alone.
  • Retained size - the bytes that would be freed if it were collected: itself plus everything only reachable through it.

A million String objects sounds alarming and usually is not - they are shared, referenced from
everywhere, and collecting any one frees 40 bytes. One HashMap with a shallow size of 48 bytes and
a retained size of 4.7 GB is your outage. It is the only thing holding those million strings,
and dropping it frees all of them.

Sort by retained size, look at the top ten, and the leak is almost always visibly there. In practice
it is one of five things:

  1. A static collection that is only ever added to
  2. Listeners or callbacks registered and never removed
  3. ThreadLocal values on a pooled thread that outlives the request
  4. A cache with no eviction policy
  5. A classloader retained by a stray reference, keeping an entire old deployment alive

The short version

  • The stack trace names the failed allocation, not the cause. Do not start there.
  • Read the message. Metaspace and Direct buffer memory are not heap problems and ignore -Xmx.
  • Use GC logs to tell a leak from an undersized heap: watch the floor after full GC, not the peak.
  • Turn on -XX:+HeapDumpOnOutOfMemoryError now, before you need it.
  • In the dump, sort by retained size. The answer is nearly always in the top ten.

KiokuGraph computes dominator trees and retained sizes for JVM and .NET
dumps, so the ranking above is the first screen rather than something you assemble by hand. Free for
a single dump - no signup needed to try it.

Top comments (0)