A thread dump answers "what is the JVM doing right now?" — and most of the problems worth catching are not about right now. A thread leak grows over hours. A connection pool saturates gradually as traffic ramps. A lock that is fine at 100 req/s becomes a serialization point at 400. Diagnosing those takes a series of dumps — and, more importantly, dumps taken at the right moment. One snapshot is a photo; monitoring is the film. This guide walks the three ways teams get from one to the other: a cron script, the JVM's own flight recorder, and trigger-based capture — including what each approach genuinely does well.
One dump is not monitoring
A single dump proves a deadlock (the JVM prints the cycle itself) and shows an obvious exhaustion if you catch it mid-incident. Everything else that matters is a delta:
- Thread leaks are a count that rises across dumps — one snapshot with 900 threads means nothing until you know it was 300 an hour ago.
- Stuck vs slow is the same question over time: a thread frozen on the same frame and lock across three dumps is hung; one that moves is just busy.
- Pool saturation has a start time — the useful question during the postmortem is when the exec pool hit its cap, which requires dumps from before the incident, not just during it.
There is a second, less obvious problem: by the time a human notices the incident and SSHes in with jstack, the interesting moment — the spike, the first minute of degradation — is often over. Manual capture is always late by the length of your reaction time. Monitoring exists to remove that lag.
The manual route: cron + jstack
The simplest thing that works: a script that dumps on an interval and rotates old files.
#!/bin/sh # /etc/cron.d/thread-dumps: every 5 min, keep 24h PID=$(pgrep -f 'org.apache.catalina.startup.Bootstrap') [ -n "$PID" ] && jstack -l "$PID" > "/var/dumps/dump-$(date +%Y%m%d-%H%M%S).txt" find /var/dumps -name 'dump-*.txt' -mmin +1440 -delete
This is honestly better than nothing, and for a slow-burn investigation (a leak over a weekend) it is fine. Its limits are structural, though:
- Fixed intervals miss the moment. A 30-second CPU spike between two 5-minute ticks leaves no trace. Shrinking the interval multiplies files, not insight.
- Nobody reads them. Hundreds of dump files accumulate unread; when the incident comes, someone greps them by hand, under pressure.
- Correlation is manual. Matching dump timestamps against the alert timeline, the deploy log and the traffic graph is busywork the script doesn't do for you.
JFR: the JVM’s built-in recorder
Java Flight Recorder is the JVM's native answer to "always be recording": a continuous, low-overhead (~1%) ring buffer of runtime events you can leave on in production and extract when something happens.
# start a continuous recording on a running JVM $ jcmd 41317 JFR.start name=continuous maxage=24h # after an incident, extract the window that matters $ jcmd 41317 JFR.dump name=continuous filename=incident.jfr
For thread analysis, JFR records execution samples, lock contention events and (on modern JDKs) periodic thread-dump events. It is the right default for profiling questions — where CPU time goes, which locks are contended over a window. Its trade-off is the workflow: the data lives in a binary .jfr file that you open in JDK Mission Control and interpret yourself. It records everything and diagnoses nothing — which is fine if reading flame graphs is your idea of a good time, and a real cost if what you need at 3 a.m. is "tell me what is wrong".
When to automate — and why triggers beat intervals
The signal that you have outgrown the manual route is repetition: the same incident class recurring (intermittent CPU spikes, a hang every few weeks), post-mortems that end with "we didn't have a dump from the right moment", or a production environment where SSH access is slow or gated. At that point the question is not whether to automate but what should pull the trigger:
- Interval-based capture (the cron above) samples time uniformly — most dumps are boring, the interesting one may not exist.
- Trigger-based capture watches the JVM's own vital signs and dumps when they cross a threshold — every dump exists because something was wrong at that instant. The JVM exposes all the signals for free via JMX: process CPU load, a built-in deadlock detector (
findDeadlockedThreads), heap occupancy, GC pause times.
Trigger-based capture is what APM-adjacent tools in this space converge on, and it is the approach worth building or buying: the dump from the first seconds of the incident is the one that shows the cause, not the aftermath.
Trigger-based capture with the ThreadMine agent
ThreadMine's capture agent is this pattern packaged: a standalone Java agent (a ~1.3 MB JAR, Java 11+, no framework dependencies) that attaches to a target JVM on the same host, watches its native MBeans every 10 seconds and captures a dump when a trigger fires:
- Sustained high CPU — process CPU above a threshold (default 80%) for several consecutive readings, so a momentary spike doesn't fire it.
- Deadlock — the JVM's own
findDeadlockedThreads; rare and critical, so this trigger bypasses the global cooldown that silences the others. - Heap saturation — usage pinned near max (default 95%) across consecutive checks.
- Long GC pauses — average pause over a sliding window (opt-in, since healthy baselines vary a lot between apps).
Cooldowns keep a persistent incident from producing a dump storm, and simultaneous triggers produce one dump labeled with every reason (e.g. [DEADLOCK] [HEAP 96%]). Configuration is generated in the web panel — target JVM, thresholds, project and tags — and downloaded as a ready-to-run package authenticated by API key.
The half that makes the capture worth automating is what happens next: each dump is analyzed on arrival by the same engine as a manual upload — a set of detectors for deadlock, pool exhaustion, thread leaks and more, plus an A–F health score — and consecutive captures line up on a timeline that shows when the degradation started. The agent ships with the Pro plan (from US$ 9/month); if you want to see what the analysis side looks like before automating anything, the free online Java thread dump analyzer takes your first dump with no signup, and the complete guide to thread dump analysis covers how to read the results.
Conclusion
Monitoring thread dumps means closing two gaps that a lone jstack leaves open: the trend (leaks, saturation and hot locks only exist across snapshots) and the moment (the dump you need is from the first seconds of the incident, before a human could react). A cron script closes neither well; JFR records continuously but leaves diagnosis to you; trigger-based capture — watching CPU, deadlock, heap and GC signals the JVM already exposes — is the approach that puts the right dump in front of you with the analysis already done. Start manual, learn what your healthy baseline looks like (the capture guide has every method), and automate when repetition tells you to.
Frequently asked questions
Is a single thread dump enough to diagnose a JVM problem?
Sometimes — a deadlock is provable from one dump, because the JVM prints the cycle itself. But the problems that hurt most in production are trends: a thread count that grows every hour, a pool that saturates under load, a lock that gets hotter as traffic ramps. Those only show up when you compare snapshots over time, which is exactly what monitoring means and a single dump cannot give you.
Does taking thread dumps hurt production performance?
Each dump briefly pauses the JVM at a safepoint — typically a few milliseconds for an application with hundreds of threads. Taken every few seconds during an incident, or on a threshold trigger with a cooldown, the overhead is negligible. What you should avoid is dumping in a tight loop with no interval; any sane schedule (or a trigger-based agent with cooldowns) keeps the cost invisible.
What triggers should fire an automatic thread dump capture?
The four that map to real incident classes: sustained high process CPU (not a single spike — several consecutive readings), a detected deadlock (ThreadMXBean.findDeadlockedThreads is authoritative and free to check), heap usage pinned near the maximum, and unusually long GC pauses. Each trigger needs a cooldown so a persistent condition produces a handful of dumps, not thousands.
Do I need a paid plan to monitor thread dumps with ThreadMine?
Manual analysis is free: the first dump does not ask for a signup, and the Free plan gives daily analyses with history. The capture agent — automatic trigger-based dumps sent from your JVMs — is part of the Pro plan, starting at US$ 9/month.
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.