"Memory leak" caused by WorkQueue#topLevelExec

David Holmes david.holmes at oracle.com
Sun Nov 30 21:28:29 UTC 2025


On 30/11/2025 12:21 am, Vicente Pader wrote:
> Hi Olivier,
> Appreciate sharing your initiative with us. In my understanding of your 
> case -
> 
> “something that behaves like a concurrent linked queue (or a lock-free 
> stack, or a tree of work items) where new work items are dynamically 
> linked together via object references.”
> 
> Let me know if this is reflective of your case.

This sounds like a response from a bot.

David
> Cheers,
> Vince
> 
> 
> On Sat, Nov 29, 2025 at 6:48 AM Olivier Peyrusse 
> <kineolyan at protonmail.com <mailto:kineolyan at protonmail.com>> wrote:
> 
>     Hello community,
> 
>     Sorry if this is the wrong place to discuss internal classes such as
>     the ForkJoinPool. If so, please, excuse me and point me in the right
>     direction.
> 
>     At my company, we have experienced an unfortunate memory leak
>     because one of our CountedCompleter was retaining a large object and
>     the task was not released to the GC (I will give more details below
>     but will first focus on the FJP code causing the issue).
> 
>     When running tasks, the FJP ends up calling WorkQueue#topLevelExec
>     <https://github.com/openjdk/jdk/blob/
>     c419dda4e99c3b72fbee95b93159db2e23b994b6/src/java.base/share/
>     classes/java/util/concurrent/ForkJoinPool.java#L1448-L1453>, which
>     is implemented as follow:
> 
>              final void topLevelExec(ForkJoinTask<?> task, int fifo) {
>                  while (task != null) {
>                      task.doExec();
>                      task = nextLocalTask(fifo);
>                  }
>              }
> 
>     We can see that it starts from a top-level task |task|​, executes
>     it, and looks for the next task to execute before repeating this
>     loop. This means that, as long as we find a task through |
>     nextLocalTask|​||​, we do not exit this method and the caller of |
>     topLevelExec|​ retains in its stack a reference to the first
>     executed task - like here <https://github.com/openjdk/jdk/blob/
>     c419dda4e99c3b72fbee95b93159db2e23b994b6/src/java.base/share/
>     classes/java/util/concurrent/ForkJoinPool.java#L1992-L2019>. This
>     acts as a path from the GC root, preventing the garbage collection
>     of the task.
>     So even if a CountedCompleter did complete its exec / tryComplete /
>     etc, the framework will keep the object alive.
>     Could the code be changed to avoid this issue? I am willing to do
>     the work, as well as come up with a test case reproducing the issue
>     if it is deemed needed.
> 
>     In our case, we were in the unfortunate situation where our counted
>     completer was holding an element which happened to be a sort of head
>     of a dynamic sort of linked queue. By retaining it, the rest of the
>     growing linked queue was also retained in memory, leading to the
>     memory leak.
>     Obvious fixes are possible in our code, by ensuring that we nullify
>     such elements when our operations complete, and more ideas. But this
>     means that we have to be constantly careful about the fields we pass
>     to the task, what is captured if we give lambdas, etc. If the whole
>     ForkJoinPool could also be improved to avoid such problems, it would
>     be an additional safety.
> 
>     Thank you for reading the mail
>     Cheers
> 
>     Olivier
> 



More information about the loom-dev mailing list