Class loader leaked when ForkJoinPool is used in loaded class, with Java 19+

Alan Bateman Alan.Bateman at oracle.com
Mon Mar 4 12:15:38 UTC 2024


core-libs-dev is the place to send this.

-Alan

On 04/03/2024 12:11, S A wrote:
> Hi all,
>
> 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.
>
> To reproduce outside of our application, we use this snippet:
>
> import java.lang.ref.WeakReference;
> import java.net.MalformedURLException;
> import java.net.URL;
> import java.net.URLClassLoader;
> import java.nio.file.Paths;
> public class TestClassloaderLeak {
>     public static void main(String[] args) throws Exception {
>         WeakReference<Object> wr = load();
>         gc();
>         System.out.println("wr=" + wr.get());
>     }
>     private static void gc() {
>         System.gc();
>         System.runFinalization();
>     }
>     private static WeakReference<Object> load() throws Exception {
>         URLClassLoader cl = new URLClassLoader(new URL[] { url() }, 
> TestClassloaderLeak.class.getClassLoader());
>         WeakReference<Object> wr = new WeakReference<>(cl);
>         Class<?> ca = cl.loadClass("A");
>         ca.getConstructor(String.class).newInstance("A");
>         cl.close();
>         return wr;
>     }
>     private static URL url() throws MalformedURLException {
>         return Paths.get("/data/tmp/testleak/lib/").toUri().toURL();
>     }
> }
>
> import java.util.concurrent.ForkJoinPool;
> import java.util.concurrent.ForkJoinTask;
> public class A {
>     public final String s;
>     public A(String s) {
>         this.s = s;
>         ForkJoinTask<?> task = ForkJoinPool.commonPool().submit(() -> 
> { System.out.println("A constructor"); });
>         try {
>             task.get();
>         } catch (Exception e) {
>             e.printStackTrace(System.out);
>         }
>     }
> }
>
> 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.
>
> 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.
>
> The leak occurs after this commit, with Java 19+:
>
> https://github.com/openjdk/jdk19u/commit/00e6c63cd12e3f92d0c1d007aab4f74915616ffb
>
> What possible workarounds can we use to avoid this problem? 
> 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.
>
> Best regards and thanks,
> Simeon



More information about the serviceability-dev mailing list