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
<[email protected] <mailto:[email protected]>> 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