On 29/11/2025 11:48 pm, Olivier Peyrusse 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.
This is better suited to [email protected].
David
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
loom-dev is about development