0

if i have one (or more) CompletableFuture not started yet, and on that method(s) a few thenApplyAsync(), anyOf()-methods.

Will the Garbage Collector remove all of that?

If there is a join()/get() at the end of that chain -> same question: Will the Garbage Collector remove all of that?

Maybe we need more information about that context of the join().

That join is in a Thread the last command, and there are no side-effects. So is in that case the Thread still active? - Java Thread Garbage collected or not

Anyway is that a good idea, to push a poisen-pill down the chain, if im sure (maybe in a try-catch-finally), that i will not start that Completable-chain, or is that not necessary?

The question is because of something like that? (https://bugs.openjdk.java.net/browse/JDK-8160402)

Some related question to it: When is the Thread-Executor signaled to shedule a new task? I think, when the CompletableFuture goes to the next chained CompletableFuture?. So i must only carry on memory-leaks and not thread-leaks?

Edit: What i mean with a not started CompletableFuture?

i mean a var notStartedCompletableFuture = new CompletableFuture<Object>(); instead of a CompletableFuture.supplyAsync(....);

I can start the notStartedCompletableFuture in that way: notStartedCompletableFuture.complete(new Object); later in the program-flow or from another thread.

Edit 2: A more detailed Example:

AtomicReference<CompletableFuture<Object>> outsideReference=new AtomicReference<>();

final var myOuterThread = new Thread(() ->
{
    final var A = new CompletableFuture<Object>();
    final var B = new CompletableFuture<Object>();

    final var C = A.thenApplyAsync((element) -> new Object());
    final var D = CompletableFuture.anyOf(A, C);

    A.complete(new Object());

    // throw new RuntimeException();

    //outsideReference.set(B);

    ----->B.complete(new Object());<------ Edit: this shouldn't be here, i remove it in my next iteration

    D.join();

});

myOuterThread.start();

//myOutherThread variable is nowhere else referenced, it's sayed so a local variable, to point on my text on it^^

  1. So in the normal case here in my example i don't have a outside
    reference. The CompletableFutures in the thread have never a chance getting completed. Normally the GC can safely erase both the thread and and the content in there, the CompetableFutures. But i don't think so, that this would happen?
  2. If I abbord this by throwing an exception -> the join() is never reached, then i think all would be erased by the GC?
  3. If I give one of the CompletableFutures to the outside by that AtomicReference, there then could be an chance to unblock the join(), There should be no GC here, until the unblock happens. BUT! the waiting myOuterThread on that join() doesen't have to to there anything more after the join(). So it could be an optimization erasing that Thread, before someone from outside completes B. But I think this would be also not happen?!

One more question here, how I can proof that behavior, if threads are blocked by waiting on a join() or are returned to a Thread-Pool?, where the Thread also "blocks"?

Robin Kreuzer
  • 162
  • 1
  • 10
  • 1
    What do you mean by _`CompletableFuture` not started yet_? Please post a snippet of code you're worried about. – Savior Nov 19 '21 at 21:47
  • @Savior, i mean a var `notStartedCompletableFuture= new CompletableFuture();` instead of a CompletableFuture.supplyAsync(....); i can start the `notStartedCompletableFuture` in that way: `notStartedCompletableFuture.complete(new Object);`, which is done later in my code and if in the meantime no exception is thrown^^. Also in the meantime i append a lot of more stages to that notStartedCompletableFuture and AnyOf's. It's a really complicated programm-flow, so i would waive on a try-catch-finally, because its not easy to spawn over a lot of classes and methods are involved here. – Robin Kreuzer Nov 19 '21 at 21:53
  • 1
    It's not clear what you are asking. Is there a code example demonstrating your problem? – DuncG Nov 20 '21 at 09:39
  • @DuncG so now i added a more detailed example, i hope now you can better understand what i mean? – Robin Kreuzer Nov 22 '21 at 00:05
  • Your thread is started and JVM has reference to it even if you don't - otherwise calls like `Thread.getAllStackTraces()` or `activeCount` wouldn't work. Therefore your thread `run` lambda will also be called at some point. – DuncG Nov 22 '21 at 08:20
  • 2
    See [here](https://stackoverflow.com/q/44530286/2711488) and [there](https://stackoverflow.com/q/61834137/2711488)… – Holger Nov 22 '21 at 11:05

3 Answers3

1

If a thread calls join() or get() on a CompletableFuture that will never be completed, it will remain blocked forever (except if it gets interrupted), holding a reference to that future.

If that future is the root of a chain of descendant futures (+ tasks and executors), it will also keep a reference to those, which will also remain in memory (as well as all transitively referenced objects).

A future does not normally hold references to its “parent(s)” when created through the then*() methods, so they should normally be garbage collected if there are no other references – but pay attention to those, e.g. local variables in the calling thread, reference to a List<CompletableFuture<?>> used in a lambda after allOf() etc.

Didier L
  • 18,905
  • 10
  • 61
  • 103
  • what you mean with "but pay attention to those, e.g. local variables in the calling thread, reference to a List> used in a lambda after allOf() etc." – Robin Kreuzer Nov 22 '21 at 11:04
  • 1
    I meant to pay attention to these other references, which might not be so obvious. A lambda used to create a downstream `CompletableFuture` may hold references to an upstream one, which would otherwise be garbage collected. – Didier L Nov 22 '21 at 12:26
  • your answer is short but as good as Stephen C one. His one goes a little bit more into detail, so i gave him the accepted answer – Robin Kreuzer Nov 24 '21 at 22:27
  • @RobinKreuzer no problem :) – Didier L Nov 24 '21 at 22:29
1

You seem to be struggling with different ways that CompletableFuture might leak, depending on how you created it. But it doesn't matter how, where, when or why it was created. The only thing that matters is whether or not it is still reachable.

Will the Garbage Collector remove all of that?

There are two places where we would expect there to be references to a CompletableFuture:

  • In the Runnable (or whatever) that would complete the future.
  • In any other code that would (at some point) attempt to get the eventual value from the future.

If you have a call thenApplyAsync() or anyOf() then the reference Runnable is in the arguments to that call. If the call can still happen, then the reference to the Runnable must still be reachable.

In your example:

var notStartedCompletableFuture = new CompletableFuture<Object>();

if the variable notStartedCompletableFuture is still accessible by some code that is still executing, then that CompletableFuture is reachable and won't be garbage collected.

On the other hand, if notStartedCompletableFuture is no longer accessible, and if the future is no longer reachable by some other path, then it won't be reachable at all ... and will be a candidate for garbage collection.


If there is a join() / get() at the end of that chain -> same question: Will the Garbage Collector remove all of that?

That makes no difference. It is all based on reachability. (The only wrinkle is that a thread that is currently alive1 is always reachable, irrespective of any other references to its Thread object. The same applies to its Runnable, and other objects reachable from the Runnable.)

But it is worth noting that if you call join() or get() on a thread / future that never terminates / completes, you will block the current thread, potentially for ever. And that is as bad as a thread leak.

1 - A thread is "alive" from when it is started to when it terminates.


When is the Thread-Executor signaled to schedule a new task?

It depends what you mean by "schedule". If you mean, when is the task submitted, the answer is when submit is called. If you mean, when is it actually run ... well it goes into the queue, and it runs when it gets to the head of the queue and a worker thread is free to execute it.

In the case of thenApplyAsync() and all_of(), the tasks are submitted (i.e. the submit(...) call occurs) when the respective method call occurs. So for example if thenApplyAsync is being called on the result of a previous call, then that call must return first.

This is all a consequence of the basic properties of Java expression evaluation ... applied to the expression that you are using to construct the chain of stages.


In general you don't need try / finally or try with resources to clean up potential memory leaks.

All you need to do is to make sure that you don't keep references to the various futures, stages, etc in variables, data structures, etc that will remain accessible / reachable beyond the lifetime of your computation. If you do that ... those references are liable to be the source of the leaks.

Thread leaks should not be your concern. If your code is not creating threads explicitly, they are being managed by the executor service / pool.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • Great Answer; i edited my question to make it more concrete, because it was asked in the comments. I saw that you edided your answer too while i did^^ - nice race condition – Robin Kreuzer Nov 22 '21 at 00:10
  • in that edit i added 3 question-points to that example, maybe you can shortly adress them, because they go a little bit into more detail at some point^^ – Robin Kreuzer Nov 23 '21 at 15:56
1

This Answer only addresses with the 3 followup questions in your "Edit 2".

  1. So in the normal case here in my example i don't have a outside reference.

I assume that you are referring to the version with the commented out statements.

The CompletableFutures in the thread have never a chance getting completed.

Incorrect. First, A is completed here:

A.complete(new Object());
    

Next B is completed here:

B.complete(new Object());

Then you call D.join(). Since D is an anyOf stage, this completes when either of A and C completes. A has already completed, so D.join() may not need to wait for C to complete. But since C applies the function asynchronously, it could complete immediately too.

Normally the GC can safely erase both the thread and and the content in there, the CompletableFutures. But I don't think so, that this would happen?

When D.join() returns, the thread terminates. At that point, its local local variables (A, B, C, and D) will be unreachable.


  1. If I abort this by throwing an exception -> the join() is never reached, then i think all would be erased by the GC?

A completes as before, but B, C and D don't.

However, the exception terminates the thread, so the local variables A, B, C, and D then become unreachable.


  1. If I give one of the CompletableFutures to the outside by that AtomicReference, there then could be an chance to unblock the join().

Three points:

  • The AtomicReference is assigned B so the join() on D is not affected.

  • As we saw above, it doesn't matter if that a hypothetical join() on outsideReference.value() happens or not for the variables A, B, C, and D. Those variables become unreachable, whichever way the thread terminates.

  • However, you have now assigned a reference to one of the CompletableFuture objects to a variable which has a different lifetime to the thread. That may mean that that CompletableFuture object stays reachable after the thread has terminated.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • ah sry no, there is a small typo in my example; i forgot to erase `B.complete(new Object());` this shouldnt be here -> and so i pointed in 1) to the not outcommented code -> and then it is going being exiting, the join would then wait theoretically forever; im sorry for that, forgotten to remove that; if you don't want to edit your answer one more time, i could understand you; bad mistake by me – Robin Kreuzer Nov 24 '21 at 22:20
  • That typo won't make any significant difference. The `D.join()` does not depend on `B` completing. Nothing in the code shown depends on `B` completing. But either way, the reasoning process is simple ... as I showed in my answer. You should have enough information to 1) understand the process, and 2) repeat it for yourself with other variations of your (artificial) example. – Stephen C Nov 24 '21 at 22:26
  • ah i see, there is the anyOf still in the game, but if it would be an allOf? Yeah i think the join would block, im not thinking the jvm is that intelligent, terminating the thread in that case, but it could – Robin Kreuzer Nov 24 '21 at 22:30
  • The JVM is not doing anything intelligent. If you manage to call `join` or equivalent on a `Future` that will never be completed (because of bugs, typos, circular dependencies, stages that go into infinite loops, whatever), that will deadlock the thread. There are liable to be leaks as a result. There's nothing the JVM can / will do about that. – Stephen C Nov 24 '21 at 22:35
  • so that means, for my edited example and allOf that we will have there an endless thread-block, ok i see; thank you – Robin Kreuzer Nov 24 '21 at 22:39