英文:
How to prevent ClassLoader leaks when using ClassValue?
问题
对于我的测试,我使用部分用Groovy编写的rest-assured。
在我的应用程序中,我使用自定义的ClassLoader来加载rest-assured和Groovy类,每次测试后,ClassLoader都会关闭并准备被GC回收。
如果我使用-Dgroovy.use.classvalue=false
选项运行测试,将使用GroovyClassValuePreJava7
类,它是ClassValue
的Groovy实现,ClassLoader会正确从堆中移除,但如果我使用-Dgroovy.use.classvalue=true
选项运行测试,将使用GroovyClassValueJava7
类,它是ClassValue
的子类,我会遇到OOME(OutOfMemoryError)。
通过分析工具,我可以看到每个ClassLoader实际上都被JNI全局引用(JNI Global Reference)保留,这些引用与每个原始类(void.class
,float.class
,boolean.class
,int.class
,double.class
,long.class
,char.class
,byte.class
)相关联。
如果我天真地对每个这些类调用InvokerHelper.removeClass(Class)
来清除ClassValueMap
,我的ClassLoader
就不再有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
类是一个简单的rest-assured测试:
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的软引用、弱引用和虚引用,但没有改变任何事情,无论引用类型如何,ClassLoader仍然存在于堆中,没有与GC根引用相关的路径。
英文:
For my tests, I'm using rest-assured which is partially written in Groovy.
In my application, I use a custom ClassLoader
to load rest-assured and Groovy classes, after each test, the ClassLoader
is closed and ready to be collected by the GC.
In case, I launch my test with -Dgroovy.use.classvalue=false
, the class GroovyClassValuePreJava7
is used which is the Groovy implementation of ClassValue
, the ClassLoader
s are properly removed from the heap but when I launch my test with -Dgroovy.use.classvalue=true
, the class GroovyClassValueJava7
is used which is a subtype of ClassValue
, I end up with an OOME.
I can see thanks to my profiler that each ClassLoader
is actually retained by a JNI Global Reference to each primitive class (void.class
, float.class
, boolean.class
, int.class
, double.class
, long.class
, char.class
, byte.class
).
If I naively call InvokerHelper.removeClass(Class)
on each of these classes, to clear the ClassValueMap
, my ClassLoaders
don't have any GC root anymore but unfortunately, they are still in the heap.
My test class is the following:
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);
}
}
The Main
class is a simple rest-assured test:
public class Main {
public static void callTest() {
RestAssured.given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("hello"));
}
}
The class CustomClassLoader
is basically an URLClassLoader
that loads jar files from a specific lib folder:
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);
}
}
I created a small project to reproduce, any idea/help is more than welcome.
What could be the root cause of this ClassLoader
leak?
I also tried to remove the soft, weak, and phantom references to the ClassLoader
s but it did not change anything, I still have the ClassLoader
s in the heap even without a path to GC roots whatever the type of reference.
答案1
得分: 1
为了修复泄漏,我们需要通过调用ClassValue#remove(Class)
来删除所有现有ClassValue
实例的关联值。在Groovy
中,我发现最简单的方法是在关闭ClassLoader
时,通过获取所有ClassInfo
并对每个底层类调用InvokerHelper.removeClass(Class)
来实现:
for (ClassInfo ci : ClassInfo.getAllClassInfo()) {
InvokerHelper.removeClass(ci.getTheClass());
}
英文:
To fix the leak, we need to remove the associated value for all existing instances of ClassValue
by calling ClassValue#remove(Class)
.
In Groovy
, the easiest way I found is by getting all the ClassInfo
and calling InvokerHelper.removeClass(Class)
on each underlying class when closing the ClassLoader
as next:
for (ClassInfo ci : ClassInfo.getAllClassInfo()) {
InvokerHelper.removeClass(ci.getTheClass());
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论