Backend Development 11 min read

Understanding Thread Pool Shutdown and Garbage Collection in Java

This article explains why a Java thread pool with many waiting threads does not release resources, how calling shutdown or shutdownNow interrupts idle workers, how the ThreadPoolExecutor internals handle interruptions, and why proper shutdown is essential to allow both threads and the pool itself to be garbage‑collected.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Understanding Thread Pool Shutdown and Garbage Collection in Java

The author discovered an application with over 900 threads, most of them in a waiting state, and investigated why the thread pool was not being reclaimed despite low CPU and memory usage.

By examining thread dump output, they identified that a thread pool whose name starts with pool contained 616 waiting threads, suggesting that the pool was the source of the problem.

Searching the code for new ThreadPoolExecutor() yielded no results, but a screenshot revealed the use of a custom new FixedTreadPool() (note the typo), which created many thread pool instances that were never shut down.

The author reproduced the issue with a simple demo:

private static void threadDontGcDemo() {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    executorService.submit(() -> {
        System.out.println("111");
    });
}

Running the demo repeatedly without calling shutdown() caused the number of threads and thread pools to continuously increase, as observed with Java VisualVM.

When shutdown() was invoked before the method finished, both the threads and the thread pool were reclaimed, confirming that proper shutdown is required for garbage collection.

The article explains that a running thread is a GC root; even threads in a waiting state are considered running and therefore prevent the pool from being collected. Calling shutdown() triggers interruptIdleWorkers() , which interrupts idle threads, causing them to throw an exception and exit.

The source code of ThreadPoolExecutor.shutdown() and related methods ( interruptIdleWorkers , runWorker , getTask , and processWorkerExit ) is examined to show how workers are interrupted, how tasks are fetched, and how workers are removed from the internal workers set, breaking the GC root chain.

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

When a worker exits abruptly, processWorkerExit removes it from the workers collection, allowing the worker object to become eligible for GC. Once all workers are removed and the surrounding request thread ends, the thread pool itself becomes an orphan and is also reclaimed.

In summary, the article advises that when a thread pool is created locally (not as a Spring bean), developers should always call shutdown() or shutdownNow() to prevent accumulation of idle threads and thread pool objects.

JavaconcurrencythreadpoolGarbageCollectionExecutorServiceshutdown
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.