High CPU in a Java process: finding the thread and the code responsible

A Java process at several hundred percent CPU is a common production alarm. The fastest route to the cause uses a simple fact: every Java platform thread is an operating system thread, so the OS can tell you which thread is busy, and a thread dump can tell you what that thread is doing.
Step 1: is it application code, GC or the JIT?
Before chasing application threads, rule out the two JVM-internal consumers:
- Garbage collection. If the heap is nearly full, collectors run back to back and their threads take the CPU. The GC log answers this in seconds: frequent collections that free little memory, or a high share of time in pauses. See reading GC logs.
- JIT compilation. Right after startup or a deployment, compiler threads can be busy for a while. That is normal and temporary. Persistent compiler activity long after startup can point at a full code cache being flushed and refilled.
The per-thread view in step 2 shows these cases too: GC threads and compiler threads have recognisable names in the dump (names containing GC, G1, C1 CompilerThread, C2 CompilerThread and so on, depending on the collector and release).
Step 2: find the busy OS threads
On Linux, top can show threads instead of processes with -H, restricted to one process with -p:
$ top -H -p <pid>
The PID column now contains thread IDs. Note the two or three at the top. For a scriptable snapshot, ps can list threads with their CPU share:
$ ps -L -o tid,pcpu -p <pid> | sort -k2 -nr | head
Step 3: convert the thread ID to hexadecimal
Thread dumps print the native thread ID as nid=0x… in hexadecimal. Convert the decimal ID from top:
$ printf '%x\n' 8011
1f4b
Step 4: take a dump and match the nid
Immediately afterwards, take a thread dump and search for that nid:
$ jcmd <pid> Thread.print -l > dump.txt
$ grep -A 20 'nid=0x1f4b ' dump.txt
The stack under that thread is what it was executing at the moment of the dump. The top frames are where the CPU is going. Repeat steps 2 to 4 two or three times: a hot loop shows the same frames each time, while a thread that just happened to be busy will not.
A shortcut: the cpu= field
Recent JDKs print cpu= (CPU time consumed) and elapsed= on each thread's header line in Thread.print output. Two dumps taken a few seconds apart give a per-thread CPU delta without using top at all, which is handy in containers that lack procps tools. A small script that parses thread names and cpu= values from both dumps and sorts by difference does the job.
From inside the application
ThreadMXBean exposes the same data programmatically through getThreadCpuTime(id), provided isThreadCpuTimeSupported() returns true. Some services expose a diagnostic endpoint that returns the top threads by CPU delta together with their stacks.
Step 5: profile if the stack is not obvious
A thread dump is a single sample. When the hot thread's stack differs from dump to dump, or when CPU is spread over many threads, you need a profile: many samples aggregated.
JDK Flight Recorder takes periodic execution samples of running threads (the jdk.ExecutionSample event) and is designed for production use. Start a recording on the running process:
$ jcmd <pid> JFR.start name=cpu settings=profile duration=2m filename=/var/tmp/cpu.jfr
# after two minutes, inspect hot methods
$ jfr print --events jdk.ExecutionSample /var/tmp/cpu.jfr | head -60
JDK Mission Control's Method Profiling page, or a flame graph view, aggregates these samples into the methods with the most CPU. See Java Flight Recorder and JDK Mission Control.
Common causes, by what the stack shows
| Top frames show | Likely cause | Next step |
|---|---|---|
java.util.regex.Pattern classes, deep recursion | Catastrophic backtracking on a pathological input | Find the pattern and the input; rewrite with possessive quantifiers or bounded repetition |
HashMap or TreeMap internals in every sample | Concurrent modification of a non-thread-safe map, which can corrupt its structure, or a map that has grown far beyond expectations | Use ConcurrentHashMap or proper locking |
| Serialisation (JSON, XML) of huge objects | An unexpectedly large payload or a reference cycle | Log payload sizes; cap response size |
A spin or retry loop calling Thread.onSpinWait or nothing at all | Busy-waiting on a condition | Block with a lock, condition or queue instead |
| Logging frames: formatting, appenders | Debug logging enabled in production, or an exception logged in a loop | Check log levels and log volume |
| GC worker threads dominate | Heap pressure | GC log; heap sizing; look for a leak |
In containers
Inside a container, top and ps may be missing, and the CPU limit changes the picture: a process limited to two CPUs that is throttled will appear slow without ever showing high CPU on the host. Check the container's throttling statistics as well as thread CPU. The JVM sizes its GC and compiler thread counts from the processors it detects; see the JVM in containers for how that detection works and how to override it with -XX:ActiveProcessorCount.
Checklist
- GC log: is the collector the one using CPU?
top -H -p <pid>: note the top thread IDs.printf '%x', then findnid=0x…in a fresh dump; repeat three times.- If stacks vary, record two minutes of JFR with
settings=profile. - Fix the code, then confirm with the same measurements.
Sources
- The jcmd command (Thread.print, JFR.start)
- The jfr command
- top(1) and ps(1) manual pages
- ThreadMXBean javadoc
Threads & CPUReading thread dumps
Take a series, read states and lock lines, and let the JVM point out deadlocks for you.
ProductionFlight Recorder and Mission Control
Continuous recording, on-demand dumps with jcmd, the events that matter, and reading .jfr files.
Garbage collectionReading GC logs with -Xlog
One line of configuration, the legacy flag mapping, and how to read pause lines for leaks and undersized heaps.