<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body style="overflow-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;">Hi. I am not really sure what you expect to happen here. When you fork/join the system needs to maintain a reference to the top-level task in order to determine when it has completed and thus the executor is available for the next. You see that in the ForkJoin JDK code you linked.<div><br></div><div>So, the easiest solution is to make the task payload clearable - you can’t clear the task but you can clear the heavy payload - allow it to be GC’d.</div><div><br></div><div>Or, make the running of the subtasks async without a reference to the heavy payload so the top-task can return and be garbage collected.</div><div><br></div><div>It sounds like a misuse of FJ - as if you are running your own FJ infra inside of ForkJoin - might as well take ForkJoin out and write a custom executor that doesn’t hold the task reference.<br id="lineBreakAtBeginningOfMessage"><div><br><blockquote type="cite"><div>On Nov 29, 2025, at 7:48 AM, Olivier Peyrusse <kineolyan@protonmail.com> wrote:</div><br class="Apple-interchange-newline"><div><div style="font-family: Arial, sans-serif; font-size: 14px;"><span>Hello community,<span style="background-color: rgb(255, 255, 255);"><span><br></span></span><br><span style="background-color: rgb(255, 255, 255);"><span>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.</span></span><div></div></span><div><br></div><div>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).</div><div><br></div><div>When running tasks, the FJP ends up calling <a href="https://github.com/openjdk/jdk/blob/c419dda4e99c3b72fbee95b93159db2e23b994b6/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java#L1448-L1453" title="WorkQueue#topLevelExec" target="_blank" rel="noreferrer nofollow noopener">WorkQueue#topLevelExec</a>, which is implemented as follow:</div><div><br></div><div><span> final void topLevelExec(ForkJoinTask<?> task, int fifo) {</span><div><span> while (task != null) {</span></div><div><span> task.doExec();</span></div><div><span> task = nextLocalTask(fifo);</span></div><div><span> }</span></div><span> }</span><br></div><div><span><br></span></div><div><font face="Arial, sans-serif">We can see that it starts from a top-level task </font><code>task</code><font face="Arial, sans-serif">, 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 </font><code>nextLocalTask</code><code></code><font face="Arial, sans-serif">, we do not exit this method and the caller of <code>topLevelExec</code> retains in its stack a reference to the first executed task - like <a href="https://github.com/openjdk/jdk/blob/c419dda4e99c3b72fbee95b93159db2e23b994b6/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java#L1992-L2019" title="here" target="_blank" rel="noreferrer nofollow noopener">here</a>. This acts as a path from the GC root, preventing the garbage collection of the task.<br>So even if a CountedCompleter did complete its exec / tryComplete / etc, the framework will keep the object alive.</font></div><div><font face="Arial, sans-serif">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.</font></div><div><font face="Arial, sans-serif"><br></font></div><div><font face="Arial, sans-serif">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.<br>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.</font></div><div><font face="Arial, sans-serif"><br></font></div><div><font face="Arial, sans-serif">Thank you for reading the mail</font></div><div><font face="Arial, sans-serif">Cheers</font></div><div><font face="Arial, sans-serif"><br></font></div><span><font face="Arial, sans-serif">Olivier</font></span><br></div>
<div style="font-family: Arial, sans-serif; font-size: 14px;" class="protonmail_signature_block protonmail_signature_block-empty">
<div class="protonmail_signature_block-user protonmail_signature_block-empty"></div>
<div class="protonmail_signature_block-proton protonmail_signature_block-empty">
</div>
</div>
</div></blockquote></div><br></div></body></html>