Search code examples
javagroovymemory-leaksclassloaderrest-assured

How to prevent ClassLoader leaks when using ClassValue?


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).

JNI Global References

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 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.


Solution

  • 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());
    }