kyle wang
kyle wang

Reputation: 1

Why debugging leads to MetaSpace memory leak in Java?

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.

The fllow image shows TenantClassLoader is referenced by JNI Global(logwire.products.crm.action.customer.ChangeCustomerOwnerUserAction), Why the ClassLoader is referenced by a Class JNI Global, and how to uninstall the ClassLoader?

enter image description here

I try to break debug when recreate TenantClassLoader, but the old TenantClassLoader still not uninstalled.

There is a minimal reproducible example, but it does not fully reproduce the online scene.

Steps to Reproduce:
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.

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;

public class TestClassLoaderLeaks {

    private static WeakReference<ClassLoader> classLoaderRef = null;

    private static void createClassLoader() throws Exception {
        ClassLoader classLoader = new MyClassLoader(new URL[]{});
        classLoaderRef = 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();
        for (int i = 0; i < 10; i++) {
            gcAndPrintClassLoaderCount(500);
        }

    }

    private static void gcAndPrintClassLoaderCount(long sleepTime) throws InterruptedException {
        System.gc();
        Thread.sleep(sleepTime);
        ClassLoader classLoader = classLoaderRef.get();
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now + ": ClassLoader : " + classLoader);
    }

    public static class MyClassLoader extends URLClassLoader {

        private Class<?> clazz;

        public MyClassLoader(URL[] urls) {
            super(urls);
        }

        @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);
        }
    }

}

JDK version:

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)

Why do I need this classloader for?
I need to frequently hot-load java code written by developers so that they can write java immediately as if they were writing JavaScript without restarting java

This happens only while debugging?
Yes, this only happens when debugging, but if we don't debug, No matter how many ClassLoaders are created and loaded, there has never been a ClassLoader leak.

Debugging tool:
IntelliJ IDEA 2024.1

Upvotes: 0

Views: 173

Answers (0)

Related Questions