You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

调试引发Java MetaSpace内存泄漏原因及ClassLoader卸载方案咨询

自定义ClassLoader调试时的JNI引用与卸载问题

问题背景

调试用户自定义ClassLoader加载的类时,该类会被JNI全局引用,导致ClassLoader无法被卸载,多次重复操作最终会引发MetaSpace溢出。

具体疑问

TenantClassLoader被JNI全局引用(关联类为logwire.products.crm.action.customer.ChangeCustomerOwnerUserAction),请问为何ClassLoader会被类的JNI全局引用?如何才能卸载该ClassLoader?

复现情况

尝试在重建TenantClassLoader时打断点调试,但旧的TenantClassLoader仍无法卸载。已准备最小复现示例,但未完全还原线上场景:

  • 复现步骤:调试源码时,若不在MyTask的run方法中添加断点,ClassLoader可正常被垃圾回收,控制台输出‘ClassLoader count: 0’;若添加断点,程序运行后ClassLoader无法被回收。

复现代码

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版本:openjdk version "17.0.10" 2024-01-16,OpenJDK Runtime Environment Temurin-17.0.10+7,OpenJDK 64-Bit Server VM Temurin-17.0.10+7(混合模式)
  • 使用场景:需频繁热加载开发者编写的Java代码,实现无需重启即可快速迭代
  • 问题触发条件:仅在调试时出现,非调试场景下多次创建加载ClassLoader无泄漏情况
  • 调试工具:IntelliJ IDEA 2024.1

问题原因

调试时ClassLoader被JNI全局引用的核心原因是调试器的底层实现逻辑:

  1. 当在类的方法上设置断点时,IDEA这类调试器会通过JNI接口将目标类的元数据(包括Class对象、关联的ClassLoader信息)注册为全局引用,目的是在断点暂停时能稳定获取栈帧、局部变量等调试数据。这些JNI全局引用不会被JVM自动回收,除非调试会话结束或手动清理相关调试上下文。
  2. Class对象本身持有其ClassLoader的强引用,一旦Class对象被JNI全局引用持有,ClassLoader就会因为存在完整的强引用链而无法被垃圾回收。

解决方案

1. 调试后主动清理调试资源

  • 调试结束后直接关闭调试会话(而非仅暂停程序),IDEA会自动释放对应的JNI全局引用,此时触发GC即可回收ClassLoader。
  • 若需持续调试,及时移除不再需要的断点,避免无用的调试上下文占用JNI引用。

2. 调整调试方式

  • 对于需要频繁热加载的类,尽量不在其方法内设置断点,改用日志打印、远程诊断等非侵入式方式排查问题。
  • 若必须断点调试,完成后立即清理断点,并手动触发Full GC(可通过jmap -histo、JConsole等工具操作)。

3. 优化ClassLoader实现

  • 确保自定义ClassLoader没有被其他强引用持有:比如避免将ClassLoader实例存在静态变量、ThreadLocal中,调试时也要注意不要让调试器的变量视图长时间保留ClassLoader引用。
  • 继承URLClassLoader时,在不再使用时调用close()方法(JDK 1.7+支持),释放内部资源,帮助GC更快识别可回收对象。

4. 调整JVM参数(可选)

  • 对于JDK 17,可尝试添加参数-XX:+UseZGC或-XX:+UseShenandoahGC,这些低延迟GC对弱引用和JNI引用的处理更高效,能更快回收未被使用的ClassLoader。
  • 关闭IDEA的"Auto reload classes"功能,避免调试器频繁关联类元数据。

内容的提问来源于stack exchange,提问作者kyle wang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 03:37:05