Sunday, 5 July 2026

How to debug 100% CPU scenario in Performance metrics

 Thread States

RUNNABLE
12
Executing on CPU
WAITING
34
Indefinitely parked
TIMED_WAITING
18
Sleeping / polling
BLOCKED
8
Waiting for monitor

The core answer for "how do you debug 100% CPU":

Start at the OS layer with top -H -p <PID> to find which native thread is hot. Convert that thread ID to hex, then find it in jstack output via the nid= field. Take 3–5 dumps, 5–10 seconds apart — if the same thread is RUNNABLE with the same stack frame in every dump, you've found your culprit (infinite loop or hot computation). If CPU is from GC, jstat -gcutil will tell you quickly.

The BLOCKED → CPU question (a classic trick question):

BLOCKED threads consume essentially zero CPU — the OS parks them. So no, a blocked thread cannot directly cause high CPU. But there's an indirect path: the thread holding the lock might be CPU-intensive, and BLOCKED threads can pile up as a symptom. The real exception is a spinlockwhile(!ready){} keeps a thread RUNNABLE and burns CPU, but that's not Java's BLOCKED state, it's a coding anti-pattern.

Deadlock vs. high CPU: These are often confused. Deadlocked threads are BLOCKED/WAITING — CPU is low or normal, but the app freezes and requests stop completing. If you see low CPU + hung requests, look for the "Found one Java-level deadlock" section at the bottom of your jstack dump.


Sample  Buggy Dump

~97% CPU
This dump was taken during a CPU spike. Click threads to inspect.
🔴Deadlock detected — Thread "db-worker-3" is blocked waiting for a lock held by "request-handler-7", which is in turn waiting for "db-worker-3".
⚠️Infinite loop suspected — "cpu-hog-thread" has been RUNNABLE for 4 consecutive dumps with 100% CPU affinity.
⚠️Lock contention — 6 threads blocked waiting for the same monitor in "OrderService".
cpu-hog-threadRUNNABLE
CPU: 98% · tid: 0x00007f3a8c · prio: 5
request-handler-7BLOCKED
Waiting for lock held by db-worker-3 · 🔒 Deadlock party A
db-worker-3BLOCKED
Waiting for lock held by request-handler-7 · 🔒 Deadlock party B
order-service-pool-1BLOCKED
Waiting for monitor in OrderService.processOrder() · +5 others
gc-worker-0TIMED_WAITING
Sleeping 50ms · GC overhead elevated at 35%
http-nio-executor-12WAITING
Parked on queue · waiting for incoming requests
"db-worker-3" #31 prio=5 os_prio=0 tid=0x00007f1c9d java.lang.Thread.State: BLOCKED (on object monitor) - waiting to lock <0x000000076b3e6c10> (a com.example.OrderLock) - locked <0x000000076b3e5b68> (a com.example.DbConnection) at com.example.DbWorker.processQuery(DbWorker.java:112) at com.example.DbWorker.run(DbWorker.java:44) 🔒 Holds: DbConnection (0x...5b68) 🔒 Wants: OrderLock (0x...6c10) — held by request-handler-7 Found one Java-level deadlock: ← jstack prints this automatically!
🔒 Deadlock party B — Holds DbConnection, wants OrderLock
Circular dependency with request-handler-7. jstack auto-detects this and prints 'Found one Java-level deadlock' at the bottom of the dump. CPU impact: ZERO. The system is frozen, not hot. Check CPU — if it's low and requests stall, suspect deadlock.


  • How to Capture



Step 1 · Find PID
Get the Java process ID
jps -l

# Or on Linux/Mac:
ps aux | grep java
Step 2 · JDK tool
jstack (most common)
jstack <PID> > dump1.txt

# Repeat 3–5 times
# 5s apart for patterns:
for i in 1 2 3; do
jstack <PID> > dump$i.txt
sleep 5
done
Step 3 · Kill signal
SIGQUIT (Linux)
kill -3 <PID>

# Dump prints to stdout
# Redirect app logs to
# capture it
Alternate · JVisualVM
GUI approach (JDK)
jvisualvm

# Connect to process
# → Threads tab
# → Thread Dump button
Alternate · JFR
Java Flight Recorder
jcmd <PID> JFR.start
duration=60s
filename=rec.jfr

# Open in JMC for
# CPU flame graph
Alternate · Async Profiler
Flame graphs (best)
./profiler.sh -d 30
-f flamegraph.html
<PID>

# Best for pinpointing
# hot methods
💡Always take 3–5 dumps, 5–10 seconds apart. A thread RUNNABLE in all dumps = likely the culprit. A single dump is a snapshot — patterns matter.

Debug Workflow

🔥Interview answer structure: Start with OS tools → correlate PID to thread → capture thread dumps repeatedly → look for RUNNABLE threads across dumps → check for locks, deadlocks, and GC overhead.
Step 1 · OS Level
Find the hot process + thread
top -H -p <PID>
# Shows per-thread CPU. Note the hot TID in decimal.
printf '%x\n' <TID> # Convert to hex — matches jstack output
Step 2 · Correlate
Map OS thread ID → jstack
jstack <PID> | grep -A 20 "nid=0x<hex_tid>"
# nid= in jstack is the native thread ID in hex
Step 3 · Pattern check
Take multiple dumps, compare
# If a thread is RUNNABLE in all 3–5 dumps → infinite loop / hot code path
# If threads are BLOCKED and growing → lock contention
# Check: "Found one Java-level deadlock" section at dump bottom
Step 4 · Root cause
Classify the problem
# Infinite loop → RUNNABLE, same stack every dump # Deadlock → BLOCKED, circular lock chain # Lock contention → many BLOCKED on same monitor # GC pressure → GC threads RUNNABLE + high GC overhead # External I/O tight loop → RUNNABLE in SocketRead/Select
Bonus · GC check
Rule out GC causing CPU
jstat -gcutil <PID> 1000 10
# GCT column growing fast → GC is burning CPU
# Check for memory leak causing constant GC

Can a BLOCKED thread cause high CPU?

Short answer: No, not directly. A BLOCKED thread is sitting idle waiting for a monitor — it uses virtually zero CPU. It is parked by the OS.
⚠️Indirect contribution: If the thread holding the lock is doing something CPU-intensive (and that causes other threads to pile up as BLOCKED), the RUNNABLE lock-holder is the CPU consumer. Blocked threads are victims, not culprits.
⚠️Spinlock exception: If your code uses busy-waiting (e.g. while(!ready){} instead of Object.wait()), the thread stays RUNNABLE and burns CPU — but that's not true Java BLOCKED state, that's a coding bug.
🔒Deadlock doesn't burn CPU — deadlocked threads are BLOCKED/WAITING, so CPU stays low. But your app freezes. Look for deadlocks when CPU is normal but requests stop completing.