Finding deadlocked threads#
When requests hang while the CPU stays idle, threads may be waiting for each other's locks. The
JVM's Threading MBean finds such a deadlock and shows which threads and locks are involved.
The output below comes from a small demo program. Its two threads, order-worker and
refund-worker, each hold one lock and wait for the other's.
Find the JVM#
Terminal
ajmx ps | jq '.result.items[] | select(.mainClass == "com.example.DeadlockDemo")'
{
"pid": 12345,
"mainClass": "com.example.DeadlockDemo",
"displayName": "DeadlockDemo"
}
--pid connects to a local JVM without remote JMX. See Local JVMs.
Find the deadlocked threads#
Terminal
ajmx --pid 12345 invoke java.lang:type=Threading findDeadlockedThreads | jq .result
{
"mbean": "java.lang:type=Threading",
"operation": "findDeadlockedThreads",
"signature": [],
"returnValue": [
25,
26
]
}
returnValue holds the IDs of the deadlocked threads. It is null when there is no deadlock.
findDeadlockedThreads covers both synchronized blocks and locks such as ReentrantLock.
findMonitorDeadlockedThreads covers synchronized blocks only.
See who holds which lock#
getThreadInfo returns the details of threads by ID. It has several overloads. describe lists
their parameter types, where [J is the JVM's name for long[].
Terminal
ajmx --pid 12345 describe java.lang:type=Threading \
| jq -c '.result.operations[] | select(.name == "getThreadInfo") | [.signature[].type]'
["[J"]
["[J","boolean","boolean"]
["[J","boolean","boolean","int"]
["[J","int"]
["long"]
["long","int"]
The parameters show up as p0, p1 and so on, without their meaning. The
ThreadMXBean Javadoc
says that getThreadInfo(long[] ids, int maxDepth) returns the threads with up to maxDepth
frames of each stack.
Pass the IDs as a JSON array, and 1 to keep the top frame only.
Terminal
ajmx --pid 12345 invoke java.lang:type=Threading getThreadInfo --args '[[25, 26], 1]' \
| jq '.result.signature, (.result.returnValue[] | {threadName, threadState, lockName, lockOwnerName, frame: .stackTrace[0] | "\(.className).\(.methodName)(\(.fileName):\(.lineNumber))"})'
[
"[J",
"int"
]
{
"threadName": "order-worker",
"threadState": "BLOCKED",
"lockName": "java.lang.Object@3502ab08",
"lockOwnerName": "refund-worker",
"frame": "com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:12)"
}
{
"threadName": "refund-worker",
"threadState": "BLOCKED",
"lockName": "java.lang.Object@21b37293",
"lockOwnerName": "order-worker",
"frame": "com.example.DeadlockDemo.lambda$main$1(DeadlockDemo.java:19)"
}
- ajmx picked the overload whose parameter types fit the arguments, and
signatureshows which one ran. If several fit, seeinvoke. lockNameis the lock the thread waits for, andlockOwnerNamethe thread that holds it. Here each thread waits for a lock the other holds.frameis the line where the thread waits.
Take a thread dump#
DiagnosticCommand runs the same commands as jcmd. threadPrint is jcmd Thread.print. It
takes its options as a String[], so pass an empty array for none, or [["-l"]] to also list the
java.util.concurrent locks each thread holds.
Terminal
ajmx --pid 12345 invoke com.sun.management:type=DiagnosticCommand threadPrint --args '[[]]' \
| jq -r .result.returnValue > threads.txt
grep -A 8 'Found one Java-level deadlock' threads.txt
Found one Java-level deadlock:
=============================
"order-worker":
waiting to lock monitor 0x00000008f2ccfb80 (object 0x00000003cfd52a50, a java.lang.Object),
which is held by "refund-worker"
"refund-worker":
waiting to lock monitor 0x00000008f2ccfaa0 (object 0x00000003cfd52a40, a java.lang.Object),
which is held by "order-worker"
The dump has the stack of every thread, and ends with this deadlock report and the stacks of the deadlocked threads.
A JVM with many threads takes longer to dump, and its dump can be larger than --max-bytes. Then
ajmx prints no dump and fails with OUTPUT_TRUNCATED. details.outputBytes is the size it needed.
Terminal
ajmx --pid 12345 --max-bytes 4096 invoke com.sun.management:type=DiagnosticCommand threadPrint --args '[[]]' \
| jq .error
{
"code": "OUTPUT_TRUNCATED",
"message": "Output exceeds --max-bytes",
"retryable": false,
"details": {
"maxBytes": 4096,
"outputBytes": 15368,
"executed": true
}
}
threadPrint only reads, so run it again with a larger --max-bytes. See
Bounded output.
These calls only read#
findDeadlockedThreads, getThreadInfo and threadPrint do not change the JVM. A thread dump
stops every thread while it is taken, though, so on a busy production JVM with many threads,
expect a short pause.