"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