Riadh Mnasri
← Back to blog
4 min read

Thread dump: usefulness, how to, and the difference from a heap dump

In the article on heap dumps, I distinguish two symptoms that look similar but call for two different tools: memory that's drifting (heap dump) and threads stuck for no apparent reason (thread dump). That second case deserves its own writeup, because it's the fastest tool to reach for and the one most often skipped in favor of a heap dump that won't show anything useful.

What a thread dump captures

A thread dump lists, for every active thread in the JVM, its state, its full call stack at the moment of the dump, and for blocked threads, the exact object (the monitor) it's waiting on and, when available, which thread currently holds it. It's a snapshot of "who's waiting on whom," not a history.

Thread stateWhat it means
RUNNABLEExecuting code or waiting on a system resource (I/O, network)
BLOCKEDWaiting to enter a synchronized section another thread currently holds
WAITINGWaiting indefinitely for an explicit signal (Object.wait(), Thread.join())
TIMED_WAITINGLike WAITING, but with a maximum delay (sleep(), wait(timeout))

Generating a thread dump

# Recommended today, doesn't require access to the full JDK install
jcmd <pid> Thread.print

# The historical tool, still widely used
jstack <pid>

# With no JDK tooling available (e.g. inside a minimal container):
# sends SIGQUIT, the JVM writes the dump to its standard output or log
kill -3 <pid>

kill -3 deserves to be known separately from the other two: in a container image that doesn't ship the full JDK tooling, it's sometimes the only option available without installing anything, since it relies on a standard Unix signal rather than a specific JDK tool.

The built-in deadlock detector

jstack and jcmd Thread.print automatically analyze blocked threads and flag a circular wait cycle in a dedicated section of the dump, typically introduced by "Found one Java-level deadlock." That's the most useful part of the tool: in the most common case (two threads blocking each other), the dump points straight at the cycle, with no need to reconstruct the chain by hand.

Found one Java-level deadlock:
=============================
"thread-A":
  waiting to lock monitor 0x00007f2b3c003988 (object 0x000000076ab62218, a Account),
  which is held by "thread-B"
"thread-B":
  waiting to lock monitor 0x00007f2b3c001a58 (object 0x000000076ab62208, a Account),
  which is held by "thread-A"

Java stack information for the threads listed above:
"thread-A":
	at Account.transfer(Account.java:45)
	- waiting to lock <0x000000076ab62218> (a Account)
	- locked <0x000000076ab62208> (a Account)
"thread-B":
	at Account.transfer(Account.java:45)
	- waiting to lock <0x000000076ab62208> (a Account)
	- locked <0x000000076ab62218> (a Account)

This classic case comes from an inconsistent lock acquisition order: thread-A locks the source account then tries to lock the destination account, while thread-B does exactly the opposite on the same two accounts. Each holds what the other is waiting for. The fix doesn't depend on the tool: it requires enforcing a total order on lock acquisition (for instance, always locking the account with the smaller ID first), so that kind of cycle becomes structurally impossible.

Telling a real stall apart from normal slowness

A single thread dump isn't always enough to conclude: a RUNNABLE thread can legitimately hold the CPU for a long time on heavy computation, without being "stuck" in any real sense. The reliable signal is taking several dumps a few seconds apart: if the same threads show up BLOCKED on the same monitors on every dump, and CPU stays flat the whole time, that's a real stall. If a RUNNABLE thread is progressing (its call stack changes from one dump to the next), that's normal processing, not an incident.

Thread dump vs heap dump, in one table

Thread dumpHeap dump
What it showsEvery thread's state and call stackThe heap's contents, object by object
Good forDeadlocks, stuck threads, lock contentionMemory leaks, abnormal heap growth
Cost to generateNear-instant, no noticeable impactCan freeze the JVM for several seconds on a large heap
Bad forDiagnosing a memory leakDiagnosing a deadlock (no information about threads)

The generation cost is worth noting: a thread dump has almost no impact on a production application, which means there's no reason to hesitate taking several in a row to compare. A heap dump, by contrast, pauses the application while it serializes the entire heap: that's a move reserved for a genuine suspicion of a leak, not a systematic reflex on every observed slowdown.