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 state | What it means |
|---|---|
| RUNNABLE | Executing code or waiting on a system resource (I/O, network) |
| BLOCKED | Waiting to enter a synchronized section another thread currently holds |
| WAITING | Waiting indefinitely for an explicit signal (Object.wait(), Thread.join()) |
| TIMED_WAITING | Like 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 dump | Heap dump | |
|---|---|---|
| What it shows | Every thread's state and call stack | The heap's contents, object by object |
| Good for | Deadlocks, stuck threads, lock contention | Memory leaks, abnormal heap growth |
| Cost to generate | Near-instant, no noticeable impact | Can freeze the JVM for several seconds on a large heap |
| Bad for | Diagnosing a memory leak | Diagnosing 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.