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