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