Hook: Interviewers love this question because high CPU is a symptom, not a diagnosis — the real skill is finding whether the fire is in your code, the JVM, or a library.
Question: How do you handle high CPU usage in a Java application?
Answer: I first confirm that the Java process is truly CPU-bound, then I identify the hottest thread or code path with a thread dump, JFR, or a profiler. After that, I look for common causes such as busy loops, lock contention, too much garbage collection, or excessive work per request. The fix is usually to block instead of spin, reduce shared-state contention, or lower the amount of repeated work.
Interview-Ready Answer: I treat high CPU as a debugging problem, not a tuning problem. First I check whether the process is actually saturating cores, then I capture a thread dump or JFR recording to find the hot threads. From there I look for busy polling, lock contention, GC pressure, or a runaway loop. Once I find the root cause, I fix the code path and verify the result with CPU, latency, and throughput metrics.
High CPU means the JVM is spending too much time running work instead of being idle. On a multi-core machine, the process may show 200%, 400%, or even 800% CPU because each busy core adds to the total. A single bad loop can max one core, while many threads fighting for a lock can spread the pain across several cores.
java process is hot, not just the whole machine. If one pod is at 95% and others are fine, the issue is likely local to that instance.top -H, jcmd Thread.print, jstack, or JFR. A thread dump is a snapshot of what every thread is doing right now; JFR is a lightweight recording that helps when the problem comes and goes.RUNNABLE often means real work, but it can also mean a spin loop or native code. BLOCKED points to lock contention. WAITING and TIMED_WAITING usually mean the thread is not the CPU hog.| Cause | Clue | Typical fix |
|---|---|---|
| Busy loop | One thread stays RUNNABLE and counts up fast | Block, sleep, or use a queue |
| Lock contention | Many threads fight on one monitor or lock | Reduce sharing, shard data, use better concurrency |
| GC pressure | Frequent collections, allocation spikes, GC threads hot | Allocate less, reuse objects, tune heap only after proof |
Use a thread dump when you need a fast answer in production. Use JFR or a profiler when the problem is intermittent or you need a longer view. A dump is roughly O(T) over the number of threads you inspect, while continuous sampling adds only small overhead, usually low enough for production use when configured carefully.
Memory hook: Think of CPU like a cashier: busy loops keep the cashier waving at an empty line, while good code lets the cashier sit until a real customer arrives.
Real-World Example: In an e-commerce checkout service, one pod suddenly pegs at 100% CPU and checkout latency jumps from 120 ms to 2 seconds. A thread dump shows a consumer thread in RUNNABLE with a tight loop polling an empty queue. The developer assumed polling was harmless, but under load the thread never blocks, so it burns a full core even when no orders are ready. Users see slow payments, autoscaling adds more pods, and the bill goes up before the bug is found. The fix is to block on the queue with a timeout, which drops CPU and keeps latency stable.
What goes wrong when misunderstood: The team may chase the wrong issue, tune the heap, or add more replicas without removing the loop. In logs, you often see little besides rising request latency and perhaps repeated timeout messages. The user impact is failed checkouts, delayed chat messages, or an API that feels “stuck” even though the service is technically alive.
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class HighCpuDemo {
public static void main(String[] args) throws Exception {
System.out.println("=== Bad: busy polling burns CPU ===");
LinkedBlockingQueue<String> badQueue = new LinkedBlockingQueue<>();
AtomicBoolean badStop = new AtomicBoolean(false);
Thread badProducer = new Thread(() -> {
sleep(500);
badQueue.offer("job-1");
sleep(500);
badStop.set(true);
}, "bad-producer");
Thread badWorker = new Thread(() -> {
long loops = 0;
long processed = 0;
long start = System.nanoTime();
while (!badStop.get()) {
loops++;
// This is the bug: when the queue is empty, we immediately try again.
// That empty path becomes a CPU burner because the thread never blocks.
String job = badQueue.poll();
if (job == null) {
continue;
}
processed++;
}
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
System.out.println("Bad worker finished in " + elapsedMs + " ms; loops=" + loops + ", processed=" + processed);
}, "bad-worker");
badWorker.start();
badProducer.start();
badWorker.join();
badProducer.join();
System.out.println();
System.out.println("=== Good: blocking wait stays idle when no work exists ===");
LinkedBlockingQueue<String> goodQueue = new LinkedBlockingQueue<>();
AtomicBoolean goodStop = new AtomicBoolean(false);
Thread goodProducer = new Thread(() -> {
sleep(500);
goodQueue.offer("job-1");
sleep(500);
goodStop.set(true);
}, "good-producer");
Thread goodWorker = new Thread(() -> {
long polls = 0;
long processed = 0;
long timeouts = 0;
long start = System.nanoTime();
while (!goodStop.get()) {
try {
// Wait a little for work instead of checking in a tight loop.
String job = goodQueue.poll(100, TimeUnit.MILLISECONDS);
polls++;
if (job == null) {
// Edge case: no work arrived during the timeout window.
// We stay calm and try again instead of spinning at full speed.
timeouts++;
continue;
}
processed++;
} catch (InterruptedException e) {
// Good services preserve interruption so shutdown works predictably.
Thread.currentThread().interrupt();
break;
}
}
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
System.out.println("Good worker finished in " + elapsedMs + " ms; polls=" + polls + ", timeouts=" + timeouts + ", processed=" + processed);
}, "good-worker");
goodWorker.start();
goodProducer.start();
goodWorker.join();
goodProducer.join();
System.out.println();
System.out.println("Takeaway: an empty loop is not 'doing nothing' — it is actively burning CPU.");
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Follow-up & Tricky Questions:
top -H or a thread dump, then map the busy native thread id back to the Java thread name. If the issue is intermittent, use JFR so you can see what happened before the spike ended.Common Mistakes:
Memory Hook: If the thread keeps asking “anything yet?” every microsecond, it is not waiting — it is wasting the cashier’s time.
Cheat Sheet:
Practice Tasks:
while(queue.isEmpty()) {} loop with BlockingQueue.take().