Let's say that in Java I have a method doSomethingAsync(input)
that schedules some work via an executor service and returns a CompletableFuture<FooBar>
. Let's say that I have a billion (or whatever huge number of distinct input
s). And let's say I chain the CompleteableFuture<FooBar>
instances together using thenCombine()
, but I don't keep a reference to the previous CompleteableFuture<FooBar>
instance. Something like this:
CompletableFuture<FooBar> future = doSomethingAsync(0);
for(int input = 1; i < 1_000_000_000; i++) {
future = future.thenCombine(doSomethingAsync(i), (foo, bar) -> bar);
}
future.join();
The interesting thing is that I can then do future.join()
to wait until they all finish. And I can set a bound (e.g. 100) on the queue on the executor service inside doSomethingAsync()
so that it makes the submission block when there are two many unfinished tasks in play. That would provides some back-pressure so that I don't run out of memory in the executor service with all 1,000,000,000 tasks being submitted to the executor service at the same time.
At the end of the process, the logic only has a reference to a single CompletableFuture<>
—the final one representing the outcome of the final submission to doSomethingAsync()
. But there were one billion of them chained together. Here's the big question: will all those 1,000,000,000 CompletableFuture<>
instances stick around in memory until the last one is finished because they were chained using thenComposeAsync()
, or will the initial CompletableFuture<>
instances be garbage collected after they are completed and the executor service submits the "next" task via thenCombine()
?
There's nothing in the public documentation about whether a CompletableFuture
can be garbage collected after completion, assuming no other "external" strong references to it exist. However, if you look at the source code then you'll find this comment1:
/* [...] * Without precautions, CompletableFutures would be prone to * garbage accumulation as chains of Completions build up, each * pointing back to its sources. So we null out fields as soon as * possible. The screening checks needed anyway harmlessly ignore * null arguments that may have been obtained during races with * threads nulling out fields. We also try to unlink non-isLive * (fired or cancelled) Completions from stacks that might * otherwise never be popped: Method cleanStack always unlinks non * isLive completions from the head of stack; others may * occasionally remain if racing with other cancellations or * removals. [...] */
And if you look through the implementation, you'll see lines like this one:
src = null; dep = null; fn = null;
So, at least as currently implemented, it looks like a CompletableFuture
becomes eligible for garbage collection after it completes, assuming you don't maintain a separate strong reference to it yourself, regardless of the subsequent chain.
1. That link is to the source code tagged jdk-20+8
. But it looks like that comment (and associated improvements) was added as part of this commit from September 2014. Perhaps as part of JDK-8056249, which was "fixed" for version 9, but looks to have been backported to Java 8