Non-intuitive disposal of objects from garbage
I am debugging a memory leak and had to dive into CompletainFuture. There is this piece of code (CompletableFuture.uniComposeStage):
CompletableFuture<V> g = f.apply(t).toCompletableFuture();
...
CompletableFuture<V> d = new CompletableFuture<V>();
UniRelay<V> copy = new UniRelay<V>(d, g);
g.push(copy);
copy.tryFire(SYNC);
return d;
The code itself is perfectly clear to me: apply a function that returns CompletionStage ( g
), create a relay that will eventually transfer the value to another CompletableFuture ( d
), and then return that other future ( d
). I see the following reference situation:
-
copy
It refers to bothd
, and ong
(and there is no magic in the constructor, only the destination fields in the constructor) -
g
linkscopy
-
d
nothing links
Returns only d
, so in fact both g
, and copy
seem to me the internal variables of the method that (seemingly) never have to leave the method and eventually be gc'd. Both naive testing and the fact that it was written by trusted developers long ago tells me that I am wrong and something is missing. What is the reason why these objects are not removed from the garbage collection?
source to share
There is nothing in the cited code to prevent the garbage collection of these futures, and there is no need. This code refers to a scenario in which the first CompletableFuture
(instance this
) has completed and the CompletableFuture
one returned by the directly handled link function is not yet complete.
Now two scenarios are possible
-
Attempting to terminate. Then the code that will eventually complete the future will contain a reference to it, and upon completion, it will initiate the completion of the dependent stages (registered via
g.push(copy)
). In this case, it is not necessary for the dependent rung to contain a link to its preliminary stage.This is a common pattern. If a chain exists
x --will complete-→ y
, the link will not be fromy
beforex
. -
There is no other reference to this
CompletableFuture
instanceg
andg
is not yet complete. In this case, it will never complete at all, and a link tog
internally won't change that. This will only waste resources.
The following sample program illustrates this:
public static void main(String[] args) throws Throwable {
ReferenceQueue<Object> discovered = new ReferenceQueue<>();
Set<WeakReference<?>> holder = new HashSet<>();
CompletableFuture<Object> initial = CompletableFuture.completedFuture("somevalue");
CompletableFuture<Object> finalStage = initial.thenCompose(value -> {
CompletableFuture<Object> lost = new CompletableFuture<>();
holder.add(new WeakReference<>(lost, discovered));
return lost;
});
waitFor(finalStage, holder, discovered);
finalStage = initial.thenCompose(value -> {
CompletableFuture<Object> saved = CompletableFuture.supplyAsync(()-> {
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
return "newvalue";
});
holder.add(new WeakReference<>(saved, discovered));
return saved;
});
waitFor(finalStage, holder, discovered);
}
private static void waitFor(CompletableFuture<Object> f, Set<WeakReference<?>> holder,
ReferenceQueue<Object> discovered) throws InterruptedException {
while(!f.isDone() && !holder.isEmpty()) {
System.gc();
Reference<?> removed = discovered.remove(100);
if(removed != null) {
holder.remove(removed);
System.out.println("future has been garbage collected");
}
}
if(f.isDone()) {
System.out.println("stage completed with "+f.join());
holder.clear();
}
}
The first function passed to thenCompose
creates and returns a new unfinished one CompletableFuture
without any attempt to complete it, without saving or keeping any other reference to it. In contrast, the second function creates CompletableFuture
through supplyAsync
, providing Supplier
, which will return the value in a second.
On my system, it prints sequentially
future has been garbage collected stage completed with newvalue
showing that an abandoned future will not be prevented by garbage collection, while another will be held at least until completion.
source to share