Usamos cookies essenciais para login e armazenamento local para suas preferências. Para métricas de uso, usamos o Google Analytics 4 — você pode recusar sem perder nada do produto. Saber mais

ThreadMineThreadMine
Back

Technical guide

Thread dump analysis: the complete guide (Java/JVM)

Thread dump analysis from scratch: how to capture the dump with jstack, jcmd or kill -3, read every thread state, and recognize the patterns behind hangs, slowdowns and high CPU.

Try it free

Thread dump analysis is the shortest path from "the application is acting weird" to "the problem is this method, on this thread, fighting over this lock." When the JVM hangs, slows to a crawl, or burns CPU without returning a response, the thread dump shows you — in plain text, with no agent and no restart — what every thread was doing at that instant. It is the cheapest and most informative diagnostic in the toolkit of anyone running Java in production.

This guide covers the full cycle: what a thread dump is (and is not), every way to generate one, the anatomy of the file line by line, what each thread state really means, a practical 5-step analysis playbook, and the problem patterns you will run into most often — from deadlocks to virtual-thread pinning.

What a thread dump is — and what it is not

A thread dump is a textual snapshot of every thread in a JVM at one specific instant. For each thread it records the name, identifiers, priority, state, the full call stack, and the locks it holds or is waiting for. Capturing one costs microseconds, does not noticeably pause the application, and requires no prior configuration — every JVM has always been able to describe itself this way.

What it is not: a CPU profile (it does not tell you how much time each method consumed, only where the thread was at the moment of the photo), a history (it shows nothing about what happened before or after), nor a picture of memory. That is why the core technique in this guide is comparing multiple dumps: it is the movement between snapshots that tells the story.

Thread dump vs. heap dump vs. core dump

The three are frequently confused, but they answer different questions. The thread dump describes execution: who is running, waiting, or blocked — kilobytes of text, safe to collect at any time. The heap dump describes memory: every live object and its references — hundreds of MB or GB, it pauses the JVM during capture, and it is the right tool for memory leaks and OutOfMemoryError. The core dump is the binary image of the entire process at the operating-system level, including native memory — used when the JVM itself dies (crash, SIGSEGV), and analyzed with tools like jhsdb. For hangs, slowness, and high CPU, the thread dump is almost always the starting point.

How to generate a thread dump

Every method below produces the dump without restarting or taking down the application. Pick one based on the access you have: a shell on the host, HTTP only, or just the console.

jstack

jstack -l <pid> > dump-$(date +%H%M%S).txt

The classic tool, shipped with the JDK. The -l flag includes the java.util.concurrent lock section (the "ownable synchronizers"), essential for investigating contention on a ReentrantLock. It must run as the same user as the JVM process. Find the pid with jps or ps.

jcmd

jcmd <pid> Thread.print

The modern Swiss Army knife of the JDK — same output as jstack, and recommended by Oracle as its replacement. On JVMs with virtual threads, prefer jcmd <pid> Thread.dump_to_file -format=json file.json, which includes the unmounted virtual threads — the classic format shows only the platform/carrier threads.

kill -3 (SIGQUIT)

kill -3 <pid>

On Linux/Unix, the JVM intercepts SIGQUIT and writes the dump to the process's own stdout — not to your terminal. Look for the result in the application log or in kubectl logs / docker logs. It is the survival option in lean containers with no JDK installed, and despite the name it does not kill the process.

Ctrl+Break (Windows)

On Windows, with the JVM running in a console, press Ctrl+Break in that window: the dump is printed to the console itself. It is the equivalent of kill -3 — for services with no console, use jstack or jcmd.

Spring Boot Actuator

GET /actuator/threaddump

If the application exposes Actuator, this endpoint returns the dump over HTTP — no shell on the host required. It returns JSON by default; with the Accept: text/plain header you get the classic format. It must be enabled in management.endpoints.web.exposure.include and, because it reveals internals, it should sit behind authentication.

VisualVM and JDK Mission Control

Graphical tools from the ecosystem: connect to the local process (or a remote one over JMX) and capture the dump with one click on the threads tab. Great on a development machine; in production there is usually no GUI and no exposed JMX port, so the command-line methods dominate.

Whatever the method, the good practice is a single one: collect 3 dumps about 10 seconds apart. One dump is a photo; three are a film. A thread stopped on the same stack across all three is stuck — a thread that changes frames is merely working.

Anatomy of a thread dump

A HotSpot dump has four parts: the JVM header, one block per thread, the lock sections, and, when present, the deadlock verdict at the end. Here is a real excerpt:

Full thread dump OpenJDK 64-Bit Server VM (21.0.2+13 mixed mode, sharing):

"http-nio-8080-exec-12" #87 daemon prio=5 os_prio=0 cpu=1042.11ms elapsed=3608.42s
    tid=0x00007f2c8c1d8000 nid=0x4e21 waiting for monitor entry [0x00007f2c5b3f6000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.exemplo.estoque.ReservaService.reservar(ReservaService.java:61)
        - waiting to lock <0x000000071a2b3c48> (a java.lang.Object)
        at com.exemplo.api.PedidoController.criar(PedidoController.java:39)

   Locked ownable synchronizers:
        - <0x000000071b9d0a10> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
  • JVM header — identifies the implementation and version (Full thread dump OpenJDK...). It is what tools use to detect the format (HotSpot, OpenJ9, Zing, GraalVM).
  • Thread line — the name in quotes (here, worker 12 of Tomcat's HTTP pool), the thread number (#87), the daemon flag, the priorities (prio, os_prio), accumulated CPU time, the tid (the JVM's internal address), and the nid — the native OS thread ID, in hexadecimal. The nid is gold in a CPU investigation: cross-reference it with the output of top -H -p <pid> to find out which Java thread is burning the processor.
  • State and stack — the java.lang.Thread.State line carries the official state, followed by the call stack from the most recent frame down to the oldest.
  • Lock section — interleaved in the stack are the - locked <address> and - waiting to lock <address> lines. The address is the monitor's identifier: threads citing the same address are fighting over the same lock. With -l, the Locked ownable synchronizers block lists the held java.util.concurrent locks.

Thread states: RUNNABLE, BLOCKED, WAITING and TIMED_WAITING

Every diagnosis starts by classifying threads by state — and knowing what each state really means (and where it misleads you).

  • RUNNABLE — running code, or ready to run as soon as the OS gives it CPU. The classic gotcha: a thread parked in socket IO (socketRead0, SocketDispatcher.read) shows up as RUNNABLE even though it is completely stopped waiting for bytes from the network — the JVM cannot see the block that happens at the operating-system level. RUNNABLE on a network stack is almost never CPU usage; RUNNABLE in a parsing, regex, or serialization loop almost always is.
  • BLOCKED — it wants to enter a synchronized block whose monitor is held by another thread. It is always contention: the waiting to lock line names the monitor, and some other thread in the dump holds the matching - locked. Many BLOCKED on the same address = a synchronization bottleneck.
  • WAITING — it called Object.wait(), LockSupport.park(), or Thread.join() without a timeout and depends on another thread to wake it. It is the natural state of idle workers waiting for a task in the pool queue — but it is suspicious when it shows up in the middle of a business-logic stack (someone waiting on a Future that never completes, for example).
  • TIMED_WAITING — the same as WAITING, but with a deadline: sleep(ms), wait(ms), poll(timeout), parkNanos. The normal state of schedulers, health checks, and queue consumers with a timeout.

Rule of thumb: state alone neither condemns nor absolves a thread. It is state + stack + the comparison across consecutive dumps that forms the diagnosis.

How to analyze in practice: a 5-step playbook

  1. Group by pool and by state. Count threads by name prefix (http-nio-*-exec-*, ForkJoinPool, custom pools) and by state. That table alone answers big questions: is the HTTP pool saturated? Is there an abnormal thread count overall?
  2. Look for a declared deadlock. Search for the string Found one Java-level deadlock at the end of the dump. When it is there, the JVM has already cross-referenced the locks for you and the diagnosis is done.
  3. Compare the 3 dumps for threads stuck on the same frame. A thread with an identical stack across all three dumps is stuck, not slow. It is the most powerful filter for separating the 12 threads that matter from the 800 that are just working.
  4. Map the contended locks. For each BLOCKED thread, follow the address in waiting to lock to the thread with the matching - locked and ask: what is the lock's owner doing? If it is in external IO, you have found the root of the queue.
  5. Correlate with the symptom. High CPU calls for focus on the compute-bound RUNNABLE threads; request timeouts call for focus on the HTTP pool; memory growth alongside threads calls for counting across dumps. The dump confirms or refutes the hypothesis — never start from the conclusion.

Problem patterns

With the playbook in hand, these are the patterns you will find in production, from the most explicit to the most subtle.

Deadlock

Two or more threads waiting on locks in crossed order — none of them ever advances. HotSpot detects the monitor cycle and prints Found one Java-level deadlock with the threads and locks involved. Deadlocks on a ReentrantLock via lock() are also declared when the dump is collected with -l. We devote an entire guide to the topic in how to detect and resolve Java deadlocks.

Pool exhaustion

Every worker thread in the pool is busy — none available for new requests, and the application "hangs" with no error in the log. In Tomcat, the sign is all of the http-nio-*-exec-* threads in RUNNABLE on IO, BLOCKED, or WAITING on external calls. The typical cause is one slow dependency with no timeout poisoning the whole pool. We cover the Spring Boot case in detail in stuck threads in Spring Boot.

CPU spike

RUNNABLE threads with a stack in pure computation — a loop with no exit condition, a catastrophic regex, serialization of huge object graphs, hash flooding. Confirm it by cross-referencing the nid with top -H: the thread at the top of the consumption is the culprit. If the same compute stack repeats across the 3 dumps, the loop is real.

Thread leak

The total thread count grows between dumps and never comes back down. It is almost always an ExecutorService created per request and never shut down, or a new Thread(...) with no pool. The terminal symptom is OutOfMemoryError: unable to create native thread. The signature in the dump: dozens of threads with sequential names (pool-847-thread-1) all idle.

Lock contention

Many BLOCKED threads pointing at the same monitor address. It does not freeze the application like a deadlock, but it serializes what should be parallel — throughput collapses and P99 explodes. The fix comes from shrinking the scope of the synchronized block, switching to concurrent data structures, or partitioning the lock.

Virtual threads (Loom): pinning and carrier starvation

With virtual threads (Java 21+), two new problems enter the radar. Pinning: up to JDK 23, a virtual thread that blocks inside a synchronized block (or in a native call) stays pinned to its carrier thread, preventing the carrier from serving other virtual threads — JDK 24 fixed the synchronized case, but native calls still pin. Carrier starvation: if every carrier is pinned or busy, thousands of ready virtual threads simply do not run. Remember that the classic dump format does not show unmounted virtual threads — use jcmd Thread.dump_to_file -format=json to see them.

Manual analysis vs. a thread dump analyzer

Manual analysis is worth it when the dump is small (dozens of threads), the case is a one-off, and you want to understand the problem in depth — it is also the best way to learn. With a text editor and the playbook above, a dump from a simple application resolves in minutes.

The math changes when the dump has hundreds or thousands of threads, when you need to compare multiple dumps against each other, when the incident recurs and every minute of diagnosis is expensive, or when the result has to become a report another team can understand. Grouping 2,000 stacks by similarity by hand is not analysis, it is penance — that mechanical work is exactly what a tool does better.

Online thread dump analyzers exist — including ThreadMine's, free and with no sign-up for your first dump. If you want to deepen your manual reading before automating, see also our guide how to analyze a Java thread dump, with a worked example interpreted step by step.

How ThreadMine automates the analysis

ThreadMine applies this entire guide in seconds. The upload accepts dumps from four JVM formats — HotSpot, OpenJ9, Zing, and GraalVM — with automatic detection from the header, so you never have to say which one it is. The content is normalized to a single model and runs through 15 detectors: declared deadlock, monitor contention, pool exhaustion, thread leak, CPU spike, clusters of identical stacks, and the other patterns described above — including virtual-thread support, with pinning detection.

Each problem becomes a card with a severity, the threads involved, and a suggested fix, and the whole set is summarized into a health score from A to F — the state of the JVM in a single letter, handy for opening an incident without attaching 4 MB of text. For the hard cases, the Vein AI analyzes the detected problems together and points to the likely root cause in natural language.

The original dump stays available for manual reading whenever you want to double-check — the tool removes the mechanical work, not your judgment.

Frequently asked questions

What is a thread dump?

A thread dump is a textual snapshot of every thread in a JVM at a single instant: the name, state, call stack, and locks held or awaited by each one. It lets you diagnose hangs, slowness, and abnormal CPU usage without restarting or instrumenting the application. Capturing one is practically instantaneous and safe for production.

How do I generate a thread dump without restarting the JVM?

Use jstack -l <pid> or jcmd <pid> Thread.print on the same host as the JVM — both print the dump without affecting the process. On Linux, kill -3 <pid> sends SIGQUIT and the JVM writes the dump to its own stdout, which is handy in containers without a JDK. None of these options stops or noticeably pauses the application.

How many thread dumps should I collect to diagnose a problem?

At least three, roughly 10 seconds apart. A single dump is a photo: it shows stopped threads that might just be working. Compare three dumps and any thread sitting on the same stack across all of them is genuinely stuck — not merely slow.

What is the difference between BLOCKED and WAITING?

BLOCKED means a thread wants to enter a synchronized block whose monitor is held by another thread — it is always lock contention. WAITING means a thread called wait(), park(), or join() without a timeout and is waiting to be woken by another thread. WAITING is normal in idle pools; BLOCKED in volume points to a synchronization bottleneck.

Is there a free online thread dump analyzer?

Yes. ThreadMine analyzes your first thread dump for free and with no sign-up: you upload it and get detected problems, a health score, and suggestions in seconds. Other online analyzers exist too, with varying depth of analysis.

Does a thread dump expose sensitive data?

It contains class, method, and thread names plus lock addresses — not variable values or memory contents, unlike a heap dump. Even so, it reveals the internal structure of your code and package names. Treat it as an internal artifact: avoid pasting it into public forums and prefer tools with a clear retention policy.

Want to see all this applied to your own dump?

Upload a dump and get a health score, detected problems and fixes in seconds. No signup for the first dump.