如何在使用ClassValue时防止ClassLoader泄漏?

huangapple go评论59阅读模式
英文:

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.classfloat.classboolean.classint.classdouble.classlong.classchar.classbyte.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 ClassLoaders 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).

如何在使用ClassValue时防止ClassLoader泄漏?

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(&quot;org.example.Main&quot;).getMethod(&quot;callTest&quot;).invoke(null);
    }
}

The Main class is a simple rest-assured test:

public class Main {
    public static void callTest() {
        RestAssured.given()
                .when().get(&quot;/hello&quot;)
                .then()
                .statusCode(200)
                .body(is(&quot;hello&quot;));
    }
}

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&lt;URI, URL&gt; toURL = uri -&gt; {
            try {
                return uri.toURL();
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            }
        };
        List&lt;URL&gt; l = Arrays.stream(
            Objects.requireNonNull(new File(&quot;./lib&quot;).listFiles())
        )
                .map(File::getAbsoluteFile)
                .map(File::toURI)
                .map(toURL).collect(Collectors.toList());
        l.add(
            toURL.apply(new File(&quot;./target/classes&quot;).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 ClassLoaders but it did not change anything, I still have the ClassLoaders 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());
}

huangapple
  • 本文由 发表于 2023年3月3日 23:37:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/75629086.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定