When I use the follow program,
import java.lang.ref.Cleaner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.LockSupport;
public class CfGc {
static final Cleaner CLEANER = Cleaner.create();
static CompletableFuture<Integer> next(CompletableFuture<Integer> f) {
Object[] status = { "not completed" };
CLEANER.register(f, () -> System.out.println(status[0]+" future collected"));
return f.whenComplete((i,t) -> {
status[0] = t != null? t: i;
LockSupport.parkNanos(500_000_000);
System.out.println(status[0]+" completed, running gc()");
System.gc();
LockSupport.parkNanos(5_000_000);
System.out.println(status[0]+" completed, gc() ran\n");
}).thenApply(i -> i + 1);
}
public static void main(String[] args) {
CompletableFuture<Integer> s = new CompletableFuture<>(), f = s;
for(int i = 0; i < 6; i++) f = next(f);
s.complete(1);
}
}
it consistently prints on my machine
1 completed, running gc()
1 completed, gc() ran
2 completed, running gc()
2 completed, gc() ran
3 completed, running gc()
2 future collected
3 completed, gc() ran
4 completed, running gc()
3 future collected
4 completed, gc() ran
5 completed, running gc()
4 future collected
5 completed, gc() ran
6 completed, running gc()
5 future collected
6 completed, gc() ran
This demonstrates that the future is reachable while its subsequent stage is evaluated, but not during the evaluation of the stage after next. It’s not like the entire chain was kept referenced until the last stage finished.
Only the first future stays reachable, which is unavoidable in the sequential evaluation of the chain, as everything happens in the complete method invoked from the main method on the first future. When we change the program to
static final Cleaner CLEANER = Cleaner.create();
static CompletableFuture<Integer> next(CompletableFuture<Integer> f) {
Object[] status = { "not completed" };
CLEANER.register(f, () -> System.out.println(status[0]+" future collected"));
return f.whenComplete((i,t) -> {
status[0] = t != null? t: i;
LockSupport.parkNanos(500_000_000);
System.out.println(status[0]+" completed, running gc()");
System.gc();
LockSupport.parkNanos(5_000_000);
System.out.println(status[0]+" completed, gc() ran\n");
}).thenApplyAsync(i -> i + 1);
}
public static void main(String[] args) {
CompletableFuture<Integer> s = new CompletableFuture<>(), f = s;
for(int i = 0; i < 6; i++) f = next(f);
s.complete(1);
s = null;
f.join();
}
it prints
1 completed, running gc()
1 completed, gc() ran
2 completed, running gc()
1 future collected
2 completed, gc() ran
3 completed, running gc()
2 future collected
3 completed, gc() ran
4 completed, running gc()
3 future collected
4 completed, gc() ran
5 completed, running gc()
4 future collected
5 completed, gc() ran
6 completed, running gc()
5 future collected
6 completed, gc() ran
on my machine, showing that the initial future can get garbage collected too when it is not referenced from a stack frame during the completion.
The same applies when we use
public static void main(String[] args) {
CompletableFuture<Integer> f = CompletableFuture.supplyAsync(() -> 1);
for(int i = 0; i < 6; i++) f = next(f);
f.join();
}
regardless of whether the next method uses thenApply or thenApplyAsync.