对于我的测试,我使用的是部分用 Groovy 编写的 rest-assured。 在我的应用程序中,我使用自定义
ClassLoader
加载 rest-assured 和 Groovy 类,每次测试后,ClassLoader
关闭并准备好被 GC 收集。
以防万一,我使用
-Dgroovy.use.classvalue=false
启动我的测试,使用类 GroovyClassValuePreJava7
这是 ClassValue
的 Groovy 实现,ClassLoader
s 被正确地从堆中移除但是当我使用 -Dgroovy.use.classvalue=true
启动我的测试
,使用类 GroovyClassValueJava7
,它是 ClassValue
的子类型,我最终得到了一个 OOME。
感谢我的分析器,我可以看到每个
ClassLoader
实际上都由JNI全局引用保留到每个原始类(void.class
,float.class
,boolean.class
,int.class
,double.class
,long.class
, char.class
, byte.class
).
如果我天真地在这些类中的每一个上调用
InvokerHelper.removeClass(Class)
,以清除ClassValueMap
,我的ClassLoaders
不再有任何GC根,但不幸的是,它们仍在堆中。
我的测试类如下:
class ClassLoaderLeakTest {
...
private CustomClassLoader cl;
@BeforeEach
void configure() {
cl = new CustomClassLoader();
}
@AfterEach
void clean() throws Exception {
cl.close();
cl = null;
}
@RepeatedTest(100)
void test() throws Exception {
cl.loadClass("org.example.Main").getMethod("callTest").invoke(null);
}
}
Main
课简单放心测试:
public class Main {
public static void callTest() {
RestAssured.given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("hello"));
}
}
类
CustomClassLoader
基本上是一个 URLClassLoader
从特定的 lib 文件夹加载 jar 文件:
public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader() {
super(urls(), null);
}
private static URL[] urls() {
Function<URI, URL> toURL = uri -> {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
};
List<URL> l = Arrays.stream(
Objects.requireNonNull(new File("./lib").listFiles())
)
.map(File::getAbsoluteFile)
.map(File::toURI)
.map(toURL).collect(Collectors.toList());
l.add(
toURL.apply(new File("./target/classes").getAbsoluteFile().toURI())
);
return l.toArray(URL[]::new);
}
}
我创建了一个小项目来重现,任何想法/帮助都非常受欢迎。
这
ClassLoader
泄漏的根本原因是什么?
我还尝试删除对
ClassLoader
s 的软引用、弱引用和虚引用,但它没有改变任何东西,即使没有通往 GC 根的路径,无论引用类型如何,我仍然在堆中保留了 ClassLoader
s .
为了修复泄漏,我们需要通过调用
ClassValue
来删除 ClassValue#remove(Class)
的所有现有实例的关联值。
在
Groovy
中,我发现最简单的方法是获取所有ClassInfo
并在关闭InvokerHelper.removeClass(Class)
时在每个基础类上调用ClassLoader
,如下所示:
for (ClassInfo ci : ClassInfo.getAllClassInfo()) {
InvokerHelper.removeClass(ci.getTheClass());
}