skip to content

Walk through how you'd use jstack together with top to find which Java code is burning CPU.

level: seniorimportance: should knowfreq 54%

answer

  1. top -H -p <pid> → hottest thread's decimal tid
  2. printf '%x' converts tid → hex nid
  3. jstack, grep nid=0x<hex> → that thread's stack = hot code
  4. Take several dumps to confirm the same frame stays hot
  5. RUNNABLE caveat + GC threads: confirm frame is real compute

basics

~20 s

Run top with per-thread view (top -H) to find the OS thread eating CPU and note its thread id. Convert that decimal id to hexadecimal. Take a jstack dump and search for that hex value as the nid in a thread's header — its stack trace shows the exact hot code.

solid answer

~50 s

The trick is correlating an OS-level busy thread with a Java stack. First, `top -H -p <pid>` shows CPU per thread inside the JVM; note the decimal thread id of the hottest one. The JVM records each thread's native id in the dump as `nid=0x...` in hexadecimal, so convert the decimal id with `printf '%x'`. Then run `jstack <pid>` and grep for `nid=0x<hex>`; the matching thread's stack trace pinpoints the method burning CPU. Because a single dump is one instant, take a few dumps a few seconds apart and confirm the same thread/frame is hot in all of them — that rules out catching it mid-something-transient. Watch the RUNNABLE caveat: a thread blocked in native I/O also shows RUNNABLE, so confirm the top frame is genuinely compute (a loop, serialization, regex) and not socketRead0. On Linux, `ps -L -o tid,pcpu -p <pid>` is an alternative to top -H. This is the canonical high-CPU triage: top-H → hex → jstack nid → hot frame.

code

java · 17 lines
java
// High-CPU triage, all from the shell over SSH:
//
// 1) Per-thread CPU inside the JVM:
//    $ top -H -p 12345        # note hottest tid, e.g. 6789 (decimal)
//
// 2) Decimal tid -> hex nid:
//    $ printf '%x\n' 6789     # -> 1a85
//
// 3) Dump and find that thread:
//    $ jstack 12345 | grep -A 12 'nid=0x1a85'
//
// Output reveals the hot frame:
// "worker-7" #31 ... nid=0x1a85 runnable
//    java.lang.Thread.State: RUNNABLE
//        at com.example.Report.render(Report.java:142)  <-- the CPU burner
//
// Repeat steps 1-3 a few seconds apart to confirm Report.render stays hot.

go deeper

for a junior

Knows jstack shows stack traces and that high CPU needs finding the busy thread, but may not know the top -H correlation.

for a middle

Can run top -H, convert tid to hex, and match nid in the dump to find the hot stack.

for a senior

Executes the full flow, takes multiple dumps, applies the native-RUNNABLE and GC-thread caveats, and knows the ps/jcmd alternatives.

for a principal

Codifies the runbook, decides when to escalate to a sampling profiler/flame graph vs spot dumps, and reasons about overhead on production.

## The problem A Java process is pegging a CPU core and you need to know *which line of Java code* is responsible. `top` tells you the **process** is busy, but a JVM has dozens of threads — you need to drill down to the **one thread** and then to its **Java stack**. jstack bridges the OS view and the JVM view. ## Step 1 — find the hot OS thread (top -H) Linux threads are scheduled as lightweight processes, each with its own **thread id (tid)**. `top` normally aggregates per process; the `-H` flag shows **per-thread** rows: ``` top -H -p 12345 ``` Sort by `%CPU` and note the **decimal tid** of the thread sitting near 100%. (Alternative without top: `ps -L -o tid,pcpu,comm -p 12345` lists each thread's tid and CPU.) ## Step 2 — convert the tid to hexadecimal The JVM identifies each thread in its dump by its **native id**, written as `nid=0x...` in **hex**, while top shows it in **decimal**. Convert: ``` printf '%x\n' 6789 # -> 1a85 ``` ## Step 3 — take a jstack dump and find that nid ``` jstack 12345 > dump.txt ``` Each thread header looks like: ``` "worker-7" #31 prio=5 os_prio=0 tid=0x... nid=0x1a85 runnable java.lang.Thread.State: RUNNABLE at com.example.Report.render(Report.java:142) ``` Search the dump for `nid=0x1a85`. The matching thread's **stack trace is the answer** — the top frames show exactly the method/line burning the CPU (here `Report.render`). ## Step 4 — confirm with multiple dumps A single dump is one instant; you might have caught a thread mid-something innocent. Take **3–5 dumps a few seconds apart** and check whether the same thread (or the same method, even if the tid differs) is RUNNABLE and hot in all of them. Consistency confirms the hot path; a frame that changes each time is normal work, not a problem. ## The RUNNABLE / native-I/O caveat A thread blocked inside a **native** call (e.g. a socket read, `socketRead0`) is reported as **RUNNABLE** even though it's idle. So before blaming a RUNNABLE thread, check the **top stack frame**: a genuine CPU burner is in compute code — a tight loop, JSON/serialization, regex backtracking, hashing, GC-heavy allocation — not in a blocking I/O frame. If the hottest thread is actually a JVM internal (e.g. a GC thread named `GC Thread#n`), your problem is garbage collection, not application code — pivot to GC logs / heap analysis. ## Why this works headless top, ps, printf and jstack are all command-line tools, so the whole flow runs over SSH on a production server with no display — a major reason this is the standard high-CPU runbook. The modern equivalent of the dump step is `jcmd <pid> Thread.print`. For sustained profiling rather than spot triage, a sampling profiler (async-profiler, VisualVM sampler) gives a flame graph, but jstack+top needs nothing installed. ## One-liner mental model **top -H → hottest tid (decimal) → printf %x → jstack, grep nid=0x… → read the stack.**

  • Why must you convert the thread id before searching the dump?
    top/ps report the native thread id in decimal, but jstack writes it as nid=0x… in hexadecimal; you convert with printf '%x' so the values match.
  • You found the hot thread, but its name is 'GC Thread#3'. What does that tell you?
    The CPU is being spent in garbage collection, not application code; pivot to GC logs, allocation rate, and heap sizing rather than the app stack.
  • Why take more than one dump?
    A single dump is one instant and may catch a thread doing something transient; several dumps over a few seconds confirm whether the same method stays hot.

saying these in an interview costs you the question

  • Forgetting the decimal→hex conversion; the dump's nid is hex but top's tid is decimal
  • Trusting a single dump; you must take several to confirm the hot frame is stable
  • Blaming a RUNNABLE thread without checking the frame — native I/O also shows RUNNABLE
  • Overlooking that the hot thread might be a GC thread, meaning the real issue is GC, not app code
  • Using plain top (process-level) instead of top -H, so you never isolate the thread

context