Back to JVM Tuning & Troubleshooting
Q12Question 12 of 15

How do you diagnose high CPU usage?

Key Concepts
  • top -H -p PID: find the thread consuming CPU
  • Convert thread ID to hex: printf "%x" TID
  • jstack PID: thread dump – find the hex TID in the output
  • Common causes: infinite loop, busy-wait, excessive GC, regex backtracking
  • JFR CPU profiling shows exactly which methods consume the most CPU

Definition

Find the hottest threads with top, take a jstack dump, match the hot native thread IDs to Java stacks, and rule out excessive GC via the GC logs.

Diagnosing high CPU in a Java process means identifying which threads are burning CPU and what code they are running. The standard technique is to find the operating-system threads consuming the most CPU, convert their native thread IDs to hexadecimal, take a thread dump with jstack, and match the hot native IDs to Java threads. Threads stuck in the RUNNABLE state doing heavy computation or spinning in an infinite loop are the usual culprits. The other major cause is excessive garbage collection, which you confirm by reading GC logs to see if GC threads are eating the CPU.

Understanding the Concepts

The reliable method links operating-system CPU usage to a specific Java thread. On Linux you run top -H -p to show per-thread CPU for the Java process. You note the thread IDs (TIDs) burning the most CPU. Those are decimal native IDs. You convert each to hexadecimal, because a Java thread dump records the native id in hex under nid=. Then you take a thread dump with jstack and search for the matching nid=0x.... The stack trace at that thread tells you exactly which Java method is running hot.

BASH
# Step 1: Find the hot thread
top -H -p 12345
# Shows: PID 12367 using 99% CPU

# Step 2: Convert to hex
printf "%x" 12367
# Output: 304f

# Step 3: Take thread dump and search
jstack 12345 > dump.txt
grep -A 30 "nid=0x304f" dump.txt
# Shows the full stack trace of the hot thread

What do you look for in the thread dump?

A thread in the RUNNABLE state in the dump is actively using CPU (or ready to). If you see the same RUNNABLE stack across several dumps taken a few seconds apart, and it is deep inside a computation or a loop, that is your hotspot.

JAVA
package com.bank.service;

public class FraudChecker {
    // This method can pin a CPU core if the condition is never met
    public boolean waitForApproval(String txnId) {
        // BAD: busy-wait spin loop with no backoff
        while (!isApproved(txnId)) {
            // No sleep, no wait – burns CPU continuously
        }
        return true;
    }

    // GOOD: proper wait with timeout
    public boolean waitForApprovalCorrectly(String txnId) throws InterruptedException {
        int retries = 0;
        while (!isApproved(txnId) && retries < 100) {
            Thread.sleep(100);  // back off between checks
            retries++;
        }
        return isApproved(txnId);
    }
}

Classic findings include an infinite or near-infinite loop, a spin on a flag without a sleep, and an unexpectedly expensive operation inside a hot path like a regular expression with catastrophic backtracking or repeated string building.

Is the CPU going into GC instead of application code?

The second major cause of high CPU is excessive garbage collection. If the application threads are not obviously hot but CPU is pinned, the GC threads may be the ones working. You confirm this by reading GC logs.

BASH
# Check GC activity
jstat -gcutil <pid> 1000
# Watch the GCT (total GC time) column
# If GC time is a large fraction of uptime, the CPU is going to collection

# Or check with GC logs
java -Xlog:gc*:file=gc.log:time -jar bank-api.jar
# Look for frequent Full GCs or very high collection time

If GC is running almost continuously and total GC time is a large fraction of uptime, the CPU is going into collection. This usually means the heap is too small, there is a leak, or allocation is far too high.

What are common mistakes when diagnosing high CPU?

The most frequent mistake is guessing which code is hot instead of measuring. Developers often start changing the code they suspect is slow without ever taking a thread dump. The second mistake is taking only a single thread dump. A single dump is a snapshot – a thread may happen to be RUNNABLE in a perfectly normal method at that instant. You need at least three dumps taken a few seconds apart to see a pattern. The third mistake is ignoring GC as a CPU consumer and blaming only application code.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

public class InterestCalculator {

    // WRONG: creates massive garbage, triggers heavy GC that looks like app CPU
    public BigDecimal calculateDailyInterestBad(BigDecimal principal, BigDecimal rate, int days) {
        BigDecimal total = BigDecimal.ZERO;
        for (int d = 0; d < days; d++) {
            // Creates a new list every iteration – pointless allocation
            List<BigDecimal> temp = new ArrayList<>();
            temp.add(principal.multiply(rate).divide(BigDecimal.valueOf(365), 10, RoundingMode.HALF_UP));
            total = total.add(temp.get(0));
        }
        return total;
    }

    // CORRECT: no unnecessary allocation, GC stays quiet
    public BigDecimal calculateDailyInterestGood(BigDecimal principal, BigDecimal rate, int days) {
        BigDecimal dailyRate = rate.divide(BigDecimal.valueOf(365), 10, RoundingMode.HALF_UP);
        BigDecimal dailyInterest = principal.multiply(dailyRate);
        return dailyInterest.multiply(BigDecimal.valueOf(days));
    }
}

In the bad version, a new ArrayList is created on every iteration for no reason, which generates enormous garbage under load. The CPU appears pinned, but the thread dumps show GC threads running, not application threads. A developer who only looks at application code would never find this – they must check GC logs first.

How do you automate CPU diagnosis in a banking production environment?

In a production banking system at National Bank of India, Rajesh cannot sit and run manual commands during a live incident. The team wraps the diagnostic steps into a shell script that triggers automatically when CPU crosses a threshold. The script captures thread dumps, GC stats, and the top output in one shot so the on-call engineer has everything needed for analysis.

BASH
#!/bin/bash
# cpu-diag.sh – automated CPU diagnosis for banking Java services
PID=$1
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUT_DIR="/var/log/bank-diagnostics/$TIMESTAMP"
mkdir -p "$OUT_DIR"

echo "Capturing diagnostics for PID $PID at $TIMESTAMP"

# Capture per-thread CPU usage
top -H -p "$PID" -b -n 1 > "$OUT_DIR/top_threads.txt"

# Take 3 thread dumps 3 seconds apart
for i in 1 2 3; do
  jstack "$PID" > "$OUT_DIR/dump_$i.txt"
  echo "Thread dump $i captured"
  sleep 3
done

# Capture GC stats
jstat -gcutil "$PID" 1000 5 > "$OUT_DIR/gcstat.txt"

# Identify the hottest thread and convert to hex
HOT_TID=$(top -H -p "$PID" -b -n 1 | awk 'NR>7{print $1; exit}')
HOT_HEX=$(printf "%x" "$HOT_TID")
echo "Hottest thread: TID=$HOT_TID hex=0x$HOT_HEX" > "$OUT_DIR/summary.txt"
grep -A 30 "nid=0x$HOT_HEX" "$OUT_DIR/dump_1.txt" >> "$OUT_DIR/summary.txt"

echo "Diagnostics saved to $OUT_DIR"

The script is triggered by an alert from the monitoring system (Prometheus, Datadog, or a simple cron check). The on-call engineer opens the summary file and immediately sees which Java method was on the CPU. This reduces incident response time from minutes of manual work to seconds of reading a pre-captured report.

What should a senior engineer know?

  • Take several thread dumps a few seconds apart rather than one. A single dump is a snapshot that might catch a thread mid-normal-work. The hotspot is the stack that keeps appearing.
  • Distinguish user CPU from system CPU. High user CPU = your code or GC computing. High system CPU = often heavy I/O or context switching.
  • async-profiler's CPU flame graph pinpoints the hot method without the manual TID matching. The widest box in the flame graph is immediately your hotspot.
  • JFR CPU profiling shows exactly which methods consume the most CPU over a recording window, with no manual thread-id matching needed.
  • In banking at National Bank of India, Rajesh diagnosed a fraud-check service that pinned a CPU core. The thread dump showed a thread stuck RUNNABLE in a retry loop that spun on a condition without any backoff. The fix was adding a proper wait with bounded retries.

Key Takeaway

When a Java process pegs the CPU, I do not guess; I link the operating system's view of CPU to a specific Java thread and the exact line it is running. On Linux I run top with the per-thread flag on the process id to see which thread is burning the most CPU, I note that thread's decimal id, and I convert it to hexadecimal, because a Java thread dump records the native id in hex. Then I take a thread dump with jstack and search for that hex id, and the stack trace right there tells me precisely which method is hot. A thread that is in the RUNNABLE state and sitting in the same deep computation or loop across two dumps taken a few seconds apart is my hotspot, and the classic finding is an infinite or spinning loop with no backoff, or an unexpectedly expensive operation like a regex with catastrophic backtracking.

The second thing I always check is whether the CPU is actually going into garbage collection rather than my own code. If no application thread looks busy but the CPU is still pinned, the GC threads are probably the ones working. I confirm that by reading the GC logs or running jstat to watch the GC time percentage, and if GC is running almost continuously the real problem is a heap that is too small, a memory leak, or an allocation rate that is far too high. I take several dumps rather than one, because a single snapshot can mislead, and for the fastest answer I use an async-profiler CPU flame graph, where the widest box is immediately the hot method with no manual id matching.

In my banking work at National Bank of India, this procedure solved two very different incidents. In one, our fraud-check service pinned a CPU core, and the thread dump showed a thread stuck RUNNABLE in a retry loop that spun on a condition that was never true with no backoff, so we fixed it by adding a proper wait and bounded retries. In another, the CPU was pinned but no application thread looked busy, and the GC logs revealed collection running nonstop because the heap was undersized for the load, so the fix was sizing the heap correctly rather than touching any application code. Knowing to separate an application hotspot from a GC-bound situation is what lets me fix the right thing fast under production pressure.

Remember one sentence: How to diagnose high CPU usage: thread dump (jstack), look for threads in RUNNABLE state doing heavy computation or infinite loops, or excessive GC (check GC logs) is fundamental knowledge for Java interviews and production banking work.

---

Share
Use ← → arrow keys to navigate