A thread dump is a snapshot of every thread in a running JVM and exactly where each one is in its call stack. It is the single most useful artifact when a Java application hangs, pegs a CPU core, stops serving requests, or slowly grinds to a halt — the stacks tell you what every thread is actually doing at that instant. The good news is that capturing one takes a few seconds and needs no restart, no profiler, and no code change. This guide covers every reliable way to take one, on any OS and in a container.
When you need a thread dump
Reach for a thread dump whenever the JVM is alive but misbehaving: requests time out but the process is up, a thread pool looks exhausted, CPU is stuck at 100% with no obvious cause, or the app deadlocks. The dump shows every thread's state (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING), the lock each blocked thread is waiting on, and which thread holds it — enough to spot a deadlock or a lock-contention pile-up directly.
Find the JVM process id (jps)
Every capture method needs the target JVM's process id. The JDK ships jps for exactly this — it lists Java processes and their main classes or jar names, so you can tell yours apart:
jps -l # 4821 com.example.MyApplication # 4977 org.jetbrains.jps.cmdline.Launcher # 5012 jdk.jcmd/sun.tools.jps.Jps # -l = full package/jar name, -v = show JVM flags
Here the app is pid 4821. If jps is not on the PATH, ps -ef | grep java (Linux/macOS) or Task Manager (Windows) work too. Note that jps only sees JVMs running as the same user — use sudo -u <user> jps to reach another user's process.
jstack <pid> — the default
jstack is the classic, dedicated tool. Point it at the pid and it prints the full dump to standard output — redirect it to a file:
jstack 4821 > dump.txt # -l adds lock info: ownable synchronizers (ReentrantLock, etc.) # and a deadlock section. Use it unless you have a reason not to. jstack -l 4821 > dump.txt # -F forces a dump from a hung/unresponsive JVM (best effort) jstack -F 4821 > dump.txt
The -l (long) flag is worth making a habit: it appends the list of locks each thread owns and a dedicated deadlock report, which is precisely what you need when threads are BLOCKED on each other.
jcmd <pid> Thread.print
jcmd is the modern, general-purpose JVM diagnostic command and the one the JDK team now recommends. Thread.print produces the identical dump:
jcmd 4821 Thread.print > dump.txt # -l for locked ownable synchronizers, same as jstack -l jcmd 4821 Thread.print -l > dump.txt # Modern virtual-thread (Project Loom) dump, JSON, JDK 21+: jcmd 4821 Thread.dump_to_file -format=json vthreads.json
The last form is the one to use on a virtual-thread application: Thread.dump_to_file enumerates virtual threads (which a classic jstack does not fully show) and its JSON output is easy for tooling to parse.
kill -3 — no tools installed
If the JDK command-line tools are not available (a stripped JRE, a locked-down box), you can still ask the JVM itself for a dump by sending it the QUIT signal:
kill -3 4821 # or, by name: kill -3 $(pgrep -f MyApplication)
The important catch: kill -3 does not write a file. The JVM prints the dump to its own stdout/console. If you launched it with java -jar app.jar > app.log 2>&1, the dump appears at the end of app.log; under a container it goes to the container logs; under systemd, to the journal. If stdout is discarded, the dump is lost — so prefer jstack/jcmd when you can. kill -3 is safe: it does not terminate the process.
On Windows
jstack and jcmd work identically on Windows — get the pid from jps or Task Manager and run the same commands. There is no kill -3; the signal equivalent is Ctrl-Break in the console window where a foreground JVM is running, which prints the dump to that console:
jps -l jstack 4821 > dump.txt rem or, for a foreground JVM, focus its console and press: Ctrl + Break
For a JVM running as a Windows service or in the background, Ctrl-Break is not an option — use jstack or jcmd with the pid.
Spring Boot Actuator
A Spring Boot application with Actuator on the classpath can expose a thread dump over HTTP — handy when you have no shell access to the host but can reach the management port. Enable the endpoint and hit it:
# application.properties
management.endpoints.web.exposure.include=threaddump
# then:
curl http://localhost:8080/actuator/threaddump # JSON
curl -H 'Accept: text/plain' \
http://localhost:8080/actuator/threaddump > dump.txt # jstack-style textThe text/plain variant returns the familiar jstack format. Guard this endpoint — a thread dump can leak internal detail, so keep the Actuator management port private or secured.
Inside a container (jattach)
In a slim container the JVM is usually pid 1 and the JDK tools are often absent. Two reliable routes:
- If the JDK is present, exec in and run the normal command:
docker exec <container> jcmd 1 Thread.print. - If only a JRE is present, use
jattach— a tiny static binary that speaks the JVM attach protocol without the full JDK:jattach 1 threaddump > dump.txt. It also driveskill -3-style dumps and works when the target runs as the same user.
Kubernetes is the same idea: kubectl exec <pod> -- jcmd 1 Thread.print > dump.txt. Because a restart wipes the evidence, capture the dump before the pod is recycled.
Take several dumps, not one
A single thread dump is a still frame. It cannot distinguish a thread that is briefly parked from one that is genuinely wedged. Capture three to five dumps, five to ten seconds apart, and compare them:
for i in 1 2 3 4 5; do jstack -l 4821 > dump-$i.txt sleep 7 done
Threads that sit on the same stack frame across every dump are the ones that are truly stuck — a hang, a deadlock, or a lock everyone is queued behind. Threads that move between dumps are just working. That comparison is the whole difference between chasing a false alarm and finding the thread that is holding everything up.
What to do with the dump
Once you have the file, reading a few thousand stack frames by hand is slow and easy to get wrong. ThreadMine parses HotSpot, OpenJ9, Zing, and GraalVM dumps, detects deadlocks, thread leaks, CPU spikes, and pool exhaustion automatically, and — when you drop in a sequence of dumps — lines them up on a timeline so the threads that never move stand out immediately. No signup is needed to analyze a dump; paste it and get the answer.
Conclusion
Capturing a thread dump is never the hard part. Find the pid with jps, then use jstack -l <pid> or jcmd <pid> Thread.print as your default; kill -3 or Ctrl-Break when no tools are installed; the Actuator endpoint when you only have HTTP; and jattach inside a container. Take several dumps a few seconds apart, and let the reading be the part you automate.
Frequently asked questions
How do I take a thread dump in Java?
The simplest way is jstack <pid>, which prints the stack trace of every thread to stdout. Find the pid with jps -l, then run jstack -l <pid> > dump.txt (the -l flag adds lock and ownable-synchronizer information). jcmd <pid> Thread.print does the same and is the recommended modern tool. If you cannot install the JDK tools, kill -3 <pid> sends the JVM a QUIT signal and it prints a dump to its own stdout/console.
What is the difference between jstack and jcmd?
They produce the same thread dump. jcmd is the newer, general-purpose diagnostic command and is the tool the JDK team recommends; jstack is the older dedicated tool and is occasionally marked as deprecated for advanced options. For a plain thread dump, jcmd <pid> Thread.print and jstack <pid> are interchangeable. jcmd can also emit a modern virtual-thread dump with Thread.dump_to_file -format=json.
How do I take a thread dump on Windows?
Use jstack <pid> or jcmd <pid> Thread.print exactly as on Linux — get the pid from jps or Task Manager. There is no kill -3 on Windows; the signal equivalent is pressing Ctrl-Break in the console window where the JVM is running in the foreground, which prints the dump to that console. jstack/jcmd are the reliable choice for a background or service JVM.
Where does kill -3 send the thread dump?
kill -3 <pid> (SIGQUIT) does not create a file. The JVM writes the dump to its own standard output — wherever the process console is directed. For an app started with java -jar app.jar > app.log, the dump lands in app.log; for a container it goes to the container logs; for a service it may go to the service log or be lost if stdout is discarded. That is why jstack/jcmd, which write to your terminal, are usually easier.
How many thread dumps should I capture?
Take at least three to five, spaced five to ten seconds apart. A single dump is a snapshot and cannot tell a thread that is briefly parked from one that is genuinely stuck. Comparing several dumps shows which threads never move (a real hang or deadlock) versus which are just busy — the difference between a false alarm and the actual problem.
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.