RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
UltraHard Scenario BasedJava#987 min readJul 11, 2026

High CPU usage in Java application.

practice
learning
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

What high CPU usually means

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.

How to debug it under the hood

  1. Confirm the symptom. Check whether the 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.
  2. Find the hot thread. Use 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.
  3. Read the thread state. 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.
  4. Classify the root cause. Ask: is the thread looping too fast, waiting too often, allocating too much, or doing repeated expensive work?
  5. Fix the cause, then prove it. Re-run the same traffic, compare CPU and p95 latency, and make sure throughput is stable. Do not stop at “CPU went down” if errors or queue delay went up.

Common causes and how they differ

CauseClueTypical fix
Busy loopOne thread stays RUNNABLE and counts up fastBlock, sleep, or use a queue
Lock contentionMany threads fight on one monitor or lockReduce sharing, shard data, use better concurrency
GC pressureFrequent collections, allocation spikes, GC threads hotAllocate less, reuse objects, tune heap only after proof

When to use which tool

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.

Important edge cases

  • A thread marked RUNNABLE is not always on-CPU; it may be in native code or waiting inside a library call.
  • One hot thread may only use one core, so the app can still look healthy on a large server until traffic grows.
  • GC can look like application CPU if allocation is extreme, so check GC logs before blaming business logic.
  • Virtual threads in modern Java reduce blocking costs, but they do not fix a busy spin loop; spinning still burns CPU.

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.

Java
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:

  • How do you find the hot thread? Start with 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.
  • What if the thread dump shows RUNNABLE but CPU is still high? That usually means the thread is either doing real work, spinning, or inside native code. I look at the stack trace to see whether it is a loop, a lock path, or a library call.
  • How do you tell GC CPU from application CPU? Check GC logs and thread stacks. If GC threads are active or collections happen very often, the app may be creating too many short-lived objects.
  • Would adding more threads fix high CPU? Not usually. More threads can make contention worse, and if the code already burns CPU in a loop, extra threads just increase the problem.
  • How do you prevent this from coming back? Add load tests, alert on CPU plus latency together, and review any polling loops or shared locks in code review.
  • Tricky: does high CPU always mean the app is broken? No. A healthy batch job or a large data sort can legitimately use a lot of CPU. The key is whether the work matches the business goal and finishes within the expected time.
  • Tricky: is blocking always bad for performance? No. Blocking is often the right choice when you are waiting for I/O or work to arrive. A blocked thread is cheaper than a spinning thread that wastes a whole core.
  • Tricky: does a thread in RUNNABLE state always mean it is on the CPU right now? No. RUNNABLE means the JVM believes it can run; the thread may still be waiting in native code, doing a syscall, or simply not scheduled yet.

Common Mistakes:

  • Blaming the JVM first. Correction: inspect stacks and GC data before changing heap flags.
  • Adding more replicas too early. Correction: scale only after you remove the CPU-burning root cause.
  • Using a tight poll loop for empty work. Correction: block with a queue, latch, or timed wait.
  • Stopping after CPU drops. Correction: also verify latency, throughput, and error rate.

Memory Hook: If the thread keeps asking “anything yet?” every microsecond, it is not waiting — it is wasting the cashier’s time.

Cheat Sheet:

  • High CPU is a symptom; find the hot thread first.
  • Use thread dumps for fast snapshots; use JFR for longer runs.
  • Look for busy loops, lock contention, GC pressure, or repeated expensive work.
  • RUNNABLE does not always mean “currently on CPU.”
  • Prefer blocking waits over empty polling loops.
  • Validate the fix with CPU, latency, and throughput together.

Practice Tasks:

  • Replace a while(queue.isEmpty()) {} loop with BlockingQueue.take().
  • Capture a thread dump from a sample app and identify the hottest stack.
  • Add a timeout and interrupt handling to a worker thread so shutdown is clean.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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(); } } }