ADDITIONAL SYSTEM INFORMATION :
openjdk version "17.0.10" 2024-01-16
OpenJDK Runtime Environment Temurin-17.0.10+7 (build 17.0.10+7)
OpenJDK 64-Bit Server VM Temurin-17.0.10+7 (build 17.0.10+7, mixed mode)
A DESCRIPTION OF THE PROBLEM :
Debugging a class loaded by a ClassLoader created by a user will cause the class to be referenced by JNI global, and the ClassLoader cannot be uninstalled. Repeating this operation for many times will eventually cause MetaSpace overflow
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Debugging the source code, if I don't add any breakpoints, then the ClassLoader can be properly garbage collected, and the console prints 'ClassLoader count: 0'. However, if I add a debugging breakpoint inside the 'run' method of 'MyTask', after the program runs, the ClassLoader is not garbage collected, and the console prints 'ClassLoader count: 2'.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The ClassLoader is properly garbage collected
ACTUAL -
The ClassLoader is not properly garbage collected
---------- BEGIN SOURCE ----------
package test;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class TestClassLoaderLeaks {
private static final List<WeakReference<ClassLoader>> classLoaderList = new ArrayList<>();
private static void createClassLoader(int count) throws Exception {
for (int i = 0; i < count; i++) {
ClassLoader classLoader = new MyClassLoader(new URL[]{}, i);
classLoaderList.add(new WeakReference<>(classLoader));
Class<?> clazz = Class.forName(MyTask.class.getName(), true, classLoader);
Constructor<?> constructor = clazz.getConstructor();
Runnable runnable = (Runnable) constructor.newInstance();
runnable.run();
}
}
public static void main(String[] args) throws Exception {
createClassLoader(2);
gcAndPrintClassLoaderCount(100);
}
private static void gcAndPrintClassLoaderCount(long sleepTime) throws InterruptedException {
System.gc();
Thread.sleep(sleepTime);
int clCount = 0;
for (WeakReference<ClassLoader> reference : classLoaderList) {
if (reference.get() != null) {
clCount++;
}
}
LocalDateTime now = LocalDateTime.now();
System.out.println(now + ": ClassLoader count: " + clCount);
}
public static class MyClassLoader extends URLClassLoader {
private final int index;
private Class<?> clazz;
public MyClassLoader(URL[] urls, int index) {
super(urls);
this.index = index;
}
public int getIndex() {
return index;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.equals(MyTask.class.getName())) {
if (clazz == null) {
String className = MyTask.class.getName();
String classPath = className.replace(".", "/") + ".class";
ClassLoader classLoader = TestClassLoaderLeaks.class.getClassLoader();
try (InputStream resourceAsStream = classLoader.getResourceAsStream(classPath)) {
if (resourceAsStream == null) {
throw new FileNotFoundException("File " + classPath + " not found");
}
ProtectionDomain protectionDomain = new ProtectionDomain(null, null);
byte[] bytes = resourceAsStream.readAllBytes();
clazz = defineClass(className, bytes, 0, bytes.length, protectionDomain);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return clazz;
} else {
return super.loadClass(name, resolve);
}
}
}
public static class MyTask implements Runnable {
@Override
public void run() {
MyClassLoader classLoader = (MyClassLoader) this.getClass().getClassLoader();
System.out.println("This is ClassLoader: " + classLoader.getIndex());
}
}
}
---------- END SOURCE ----------
FREQUENCY : always
openjdk version "17.0.10" 2024-01-16
OpenJDK Runtime Environment Temurin-17.0.10+7 (build 17.0.10+7)
OpenJDK 64-Bit Server VM Temurin-17.0.10+7 (build 17.0.10+7, mixed mode)
A DESCRIPTION OF THE PROBLEM :
Debugging a class loaded by a ClassLoader created by a user will cause the class to be referenced by JNI global, and the ClassLoader cannot be uninstalled. Repeating this operation for many times will eventually cause MetaSpace overflow
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Debugging the source code, if I don't add any breakpoints, then the ClassLoader can be properly garbage collected, and the console prints 'ClassLoader count: 0'. However, if I add a debugging breakpoint inside the 'run' method of 'MyTask', after the program runs, the ClassLoader is not garbage collected, and the console prints 'ClassLoader count: 2'.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The ClassLoader is properly garbage collected
ACTUAL -
The ClassLoader is not properly garbage collected
---------- BEGIN SOURCE ----------
package test;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class TestClassLoaderLeaks {
private static final List<WeakReference<ClassLoader>> classLoaderList = new ArrayList<>();
private static void createClassLoader(int count) throws Exception {
for (int i = 0; i < count; i++) {
ClassLoader classLoader = new MyClassLoader(new URL[]{}, i);
classLoaderList.add(new WeakReference<>(classLoader));
Class<?> clazz = Class.forName(MyTask.class.getName(), true, classLoader);
Constructor<?> constructor = clazz.getConstructor();
Runnable runnable = (Runnable) constructor.newInstance();
runnable.run();
}
}
public static void main(String[] args) throws Exception {
createClassLoader(2);
gcAndPrintClassLoaderCount(100);
}
private static void gcAndPrintClassLoaderCount(long sleepTime) throws InterruptedException {
System.gc();
Thread.sleep(sleepTime);
int clCount = 0;
for (WeakReference<ClassLoader> reference : classLoaderList) {
if (reference.get() != null) {
clCount++;
}
}
LocalDateTime now = LocalDateTime.now();
System.out.println(now + ": ClassLoader count: " + clCount);
}
public static class MyClassLoader extends URLClassLoader {
private final int index;
private Class<?> clazz;
public MyClassLoader(URL[] urls, int index) {
super(urls);
this.index = index;
}
public int getIndex() {
return index;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.equals(MyTask.class.getName())) {
if (clazz == null) {
String className = MyTask.class.getName();
String classPath = className.replace(".", "/") + ".class";
ClassLoader classLoader = TestClassLoaderLeaks.class.getClassLoader();
try (InputStream resourceAsStream = classLoader.getResourceAsStream(classPath)) {
if (resourceAsStream == null) {
throw new FileNotFoundException("File " + classPath + " not found");
}
ProtectionDomain protectionDomain = new ProtectionDomain(null, null);
byte[] bytes = resourceAsStream.readAllBytes();
clazz = defineClass(className, bytes, 0, bytes.length, protectionDomain);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return clazz;
} else {
return super.loadClass(name, resolve);
}
}
}
public static class MyTask implements Runnable {
@Override
public void run() {
MyClassLoader classLoader = (MyClassLoader) this.getClass().getClassLoader();
System.out.println("This is ClassLoader: " + classLoader.getIndex());
}
}
}
---------- END SOURCE ----------
FREQUENCY : always