<div dir="ltr"><div dir="ltr"><div>Hi all,</div><div><br></div><div>after moving our 
application to Java 21 (up from Java 17), we noticed a class loader 
leak. A memory snapshot points to a ProtectionDomain object held by a 
ForkJoinWorkerThread, the protection domain holds the class loader and 
prevents GC.</div><div><br></div><div>To reproduce outside of our application, we use this snippet:</div><div><br></div><div>import java.lang.ref.WeakReference;<br>import java.net.MalformedURLException;<br>import java.net.URL;<br>import java.net.URLClassLoader;<br>import java.nio.file.Paths;<br>public class TestClassloaderLeak {<br>    public static void main(String[] args) throws Exception {<br>        WeakReference<Object> wr = load();<br>        gc();<br>        System.out.println("wr=" + wr.get());<br>    }<br>    private static void gc() {<br>        System.gc();<br>        System.runFinalization();<br>    }<br>    private static WeakReference<Object> load() throws Exception {<br>        URLClassLoader cl = new URLClassLoader(new URL[] { url() }, TestClassloaderLeak.class.getClassLoader());<br>        WeakReference<Object> wr = new WeakReference<>(cl);<br>        Class<?> ca = cl.loadClass("A");<br>        ca.getConstructor(String.class).newInstance("A");<br>        cl.close();<br>        return wr;<br>    }<br>    private static URL url() throws MalformedURLException {<br>        return Paths.get("/data/tmp/testleak/lib/").toUri().toURL();<br>    }<br>}<br></div><div><br></div><div>import java.util.concurrent.ForkJoinPool;<br>import java.util.concurrent.ForkJoinTask;<br>public class A {<br>    public final String s;<br>    public A(String s) {<br>        this.s = s;<br>        ForkJoinTask<?> task = ForkJoinPool.commonPool().submit(() -> { System.out.println("A constructor"); });<br>        try {<br>            task.get();<br>        } catch (Exception e) {<br>            e.printStackTrace(System.out);<br>        }<br>    }<br>}</div><div><br></div><div>Place
 the compiled A.class at the hard-coded location 
"/data/tmp/testleak/lib/", then run the main method with JVM flags 
"-Xlog:class+unload". Observe that no class is unloaded, which is not 
the case if the main() code runs twice, or if the snippet is executed 
e.g. with Java 17.<br></div><div><br></div><div>It seems each time 
ForkJoinPool creates a new ForkJoinWorkerThread from a loaded class, it 
is no longer possible to GC the class loader of the class using 
ForkJoinPool.</div><div><br></div><div><div>The leak occurs after this commit, with Java 19+:</div><div><br></div><div><a href="https://github.com/openjdk/jdk19u/commit/00e6c63cd12e3f92d0c1d007aab4f74915616ffb" target="_blank">https://github.com/openjdk/jdk19u/commit/00e6c63cd12e3f92d0c1d007aab4f74915616ffb</a></div></div><div><br></div><div>Essentially, our
 application loads and runs user code. The user changes their code, runs
 their code again - we use a new class loader to run the changed code in
 the same JVM. We unload the previous class loader, to free up memory 
and to avoid problems with hot swapping code (an old version of a loaded
 class can cause an error when trying to hot swap the latest loaded 
version of that class). So the leaked class loader is giving us trouble.<br></div><div><br></div><div>What
 possible workarounds can we use to avoid this problem?</div><div><br></div><div>Best regards and thanks,</div><div>Simeon<div class="gmail-yj6qo"></div><div class="gmail-adL"><br><br></div></div></div></div>