Backend Development 6 min read

Avoiding OOM When Using java.util.concurrent.ExecutorCompletionService

The article explains how submitting tasks to ExecutorCompletionService without retrieving their results causes the internal unbounded LinkedBlockingQueue to retain Future objects, leading to memory leaks and OutOfMemoryError, and demonstrates the correct usage patterns to prevent this issue.

Cognitive Technology Team
Cognitive Technology Team
Cognitive Technology Team
Avoiding OOM When Using java.util.concurrent.ExecutorCompletionService

When using java.util.concurrent.ExecutorCompletionService to execute tasks asynchronously, developers often call the #submit(Callable<V>) or #submit(Runnable, V) methods but neglect to retrieve the results with #take() or #poll() . This omission leaves the completed Future objects in the internal LinkedBlockingQueue , which is unbounded, causing memory to grow until an OutOfMemoryError (OOM) occurs.

The class constructs a completion queue as a new LinkedBlockingQueue<Future<V>>() . Each submitted task is wrapped in a QueueingFuture that, upon completion, places its result into this queue. Because the queue has no size limit, every finished task accumulates unless explicitly removed.

To avoid the leak, after a task finishes you must remove its result from the queue by calling one of the provided methods:

public Future
take() throws InterruptedException { return completionQueue.take(); }
public Future
poll() { return completionQueue.poll(); }
public Future
poll(long timeout, TimeUnit unit) throws InterruptedException { return completionQueue.poll(timeout, unit); }

A proper usage pattern is to submit all tasks, then repeatedly call cs.take().get() to obtain and process each result, ensuring the queue is drained:

void solve(Executor e, Collection<Callable<Result>> solvers) throws InterruptedException, ExecutionException {
    CompletionService<Result> cs = new ExecutorCompletionService<>(e);
    solvers.forEach(cs::submit);
    for (int i = solvers.size(); i > 0; i--) {
        Result r = cs.take().get();
        if (r != null) {
            // process result
        }
    }
}

An alternative example shows retrieving the first completed result and cancelling the remaining futures, but it still carries a risk of leaking results if some tasks finish after cancellation because QueueingFuture does not override cancel to remove its entry from the queue.

In summary, always invoke #take() or #poll() for every submitted task when using ExecutorCompletionService , or consider avoiding this class altogether, as its implementation is not authored by Doug Lea and may lead to subtle memory‑leak bugs.

javaConcurrencythreadpoolMemoryLeakoomExecutorCompletionService
Cognitive Technology Team
Written by

Cognitive Technology Team

Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.

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.