Reading Java thread dumps: jstack, jcmd Thread.print and what each line means

When a Java service hangs, slows down or stops answering health checks, the first thing to capture is a thread dump: a snapshot of every thread, its state and its stack. It is cheap, it does not need any agent, and read correctly it often names the culprit outright.
Taking a dump
The JDK offers three equivalent routes:
# recommended: jcmd, with java.util.concurrent lock details
$ jcmd <pid> Thread.print -l > dump-1.txt
# jstack, same content
$ jstack -l <pid> > dump-1.txt
# SIGQUIT: the JVM prints the dump on its own standard output
$ kill -3 <pid>
The JDK troubleshooting guide suggests jcmd rather than jstack for enhanced diagnostics and reduced overhead. The -l option adds the ownable synchronizers (ReentrantLock and friends) held by each thread; without it, only monitors (synchronized) are shown. The jcmd reference also lists -e for extended thread information.
kill -3 does not stop the process. It is useful when the attach mechanism fails, for example when the tool runs as a different user than the JVM. The output goes wherever the JVM's stdout goes, which in a container is the container log.
Always take a series
One dump is a photograph; a problem is a film. Take three to five dumps a few seconds apart. A thread that shows the same stack in every dump is stuck or doing a long operation; a thread that changes is making progress.
$ for i in 1 2 3 4 5; do jcmd <pid> Thread.print -l > dump-$i.txt; sleep 5; done
Anatomy of a thread entry
"http-worker-17" #84 daemon prio=5 os_prio=0 cpu=…ms elapsed=…s tid=0x00007f... nid=0x1f2b waiting for monitor entry [0x00007f...]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.cache.Registry.lookup(Registry.java:88)
- waiting to lock <0x00000006c1a3e2f8> (a java.util.HashMap)
at com.example.api.OrderHandler.handle(OrderHandler.java:41)
...
Reading left to right on the first line:
- Name in quotes. Name your pools (a
ThreadFactorythat sets meaningful names) and dumps become far easier to read. - #84, the Java thread ID; daemon if it is a daemon thread.
- cpu= and elapsed= on recent JDKs: CPU time consumed by the thread and time since it started. Comparing
cpuacross dumps shows which threads are burning CPU (see finding the thread behind high CPU). - nid, the native (operating system) thread ID, in hexadecimal.
- A short status, followed by the
java.lang.Thread.Stateline.
Thread states
The states are those of Thread.State:
| State | Meaning | What to look for |
|---|---|---|
| RUNNABLE | Executing in the JVM, or in native code such as a socket read | A RUNNABLE thread in socketRead is waiting for the network, not using CPU |
| BLOCKED | Waiting to enter a synchronized block | Find the thread that holds the monitor |
| WAITING | Waiting indefinitely: Object.wait(), LockSupport.park(), Thread.join() | Idle pool threads look like this; that is normal |
| TIMED_WAITING | The same with a timeout, or Thread.sleep | Retry loops and polling |
| NEW, TERMINATED | Not started, finished | Rarely appear in dumps |
Lock lines
The lines starting with a dash connect threads to each other:
- locked <0x...> (a ...): this frame holds the monitor of that object.- waiting to lock <0x...>: blocked trying to enter it.- waiting on <0x...>: insideObject.wait()on that object (the monitor was released while waiting).- parking to wait for <0x...> (a java.util.concurrent.locks.ReentrantLock$NonfairSync): waiting for ajava.util.concurrentlock or condition.- With
-l, aLocked ownable synchronizers:section under each thread lists thejava.util.concurrentlocks it holds.
To find a bottleneck, search the dump for the address in a waiting to lock line and find the single thread whose stack shows locked with the same address. That thread's stack is the code holding everyone else up.
Deadlocks
HotSpot detects Java-level deadlocks itself. When one exists, the end of the dump contains a section starting with Found one Java-level deadlock:, listing the threads involved, the lock each one waits for and which thread owns it, followed by their stacks. Detection covers monitors and, with -l, ownable synchronizers. The same check is available programmatically through ThreadMXBean.findDeadlockedThreads(), which a health check can call.
Classic causes and fixes:
- Inconsistent lock order. Thread A takes X then Y, thread B takes Y then X. Fix: one global order for nested locks.
- Calling unknown code while holding a lock (a listener, a callback, a logging appender). Fix: copy what you need, release the lock, then call out.
- Waiting forever. Prefer
tryLock(timeout, unit)and timeouts onFuture.getso that a stuck dependency becomes an error instead of a hang.
Thread pools that starve themselves
Not every hang is a lock cycle. A task running in a bounded pool that submits a sub-task to the same pool and waits for its result can block forever once all workers are doing the same: every worker waits for a sub-task that has no free worker to run it. The dump shows every pool thread in WAITING inside FutureTask.get and nothing deadlocked in the JVM's sense. Use separate pools for dependent stages, or a non-blocking composition such as CompletableFuture.
Also recall how ThreadPoolExecutor grows: below the core size it creates threads; above it, it queues; only when the queue is full does it create threads up to the maximum; beyond that it rejects. With an unbounded queue the maximum size is never reached, and work piles up in the queue rather than in the dump.
Virtual threads
Thread.print shows platform threads. For applications using virtual threads, JEP 444 introduced a dump format that includes them, written to a file, optionally as JSON:
$ jcmd <pid> Thread.dump_to_file -format=json /var/tmp/threads.json
See virtual threads in production.
Sources
- Java Platform Troubleshooting Guide: Diagnostic Tools
- The jcmd command and the jstack command
- Thread.State, ThreadMXBean, ThreadPoolExecutor
- JEP 444: Virtual Threads
Threads & CPUHigh CPU: find the thread
From top -H to the nid in a thread dump: a repeatable way to name the code that is burning CPU.
Threads & CPUVirtual threads in production
Carriers, pinning before and after JDK 24, why not to pool them, and how to see them in dumps and JFR.
MemoryTaking and reading a heap dump
Capture safely with jcmd, dump automatically on OOM, then read the dominator tree and paths to GC roots.