栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

实战之ClassLoader动态加载插件无法回收引用排查

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

实战之ClassLoader动态加载插件无法回收引用排查

最近在看jvm-sandbox的一些功能,参考着实现了动态加载Jar包插件的功能,但是实现的这个功能有一个比较严重的问题,就是类加载完毕之后,当你需要覆盖或者卸载时候,该类加载器的引用是无法被回收的。也就是说由这个类加载器加载之后,无法卸载,这个加载器一直存在。

如果一旦新增或者覆盖的jar包过多,会导致类加载器一直堆积。严重点会发生泄漏的风险。

基于以上场景开始了漫漫排查路。

代码回顾 1. 自定义的类加载器

这个加载器的主要功能是负责路由,也是参考的jvm-sandbox。
主要目的是将加载器隔离:比如主加载器A,插件加载器为B

同样一个接口A加载器肯定是有的,B加载器也有,如果各自加载那么同一个类也会出现不一致。所以为了保证全局唯一,有一些特定的类B中即便有的话也需要从A中去加载。这就是这个路由的意义。

public class ManagerClassLoader extends URLClassLoader {

    private final Logger logger = LoggerFactory.getLogger(ManagerClassLoader.class);
    private final Routing[] routingArray;

    public ManagerClassLoader(final URL[] urls,
                              final Routing... routingArray) {
        super(urls);
        this.routingArray = routingArray;
    }

    public ManagerClassLoader(final URL[] urls,
                              final ClassLoader parent,
                              final Routing... routingArray) {
        super(urls, parent);
        this.routingArray = routingArray;
    }

    @Override
    public URL getResource(String name) {
        URL url = findResource(name);
        if (null != url) {
            return url;
        }
        url = super.getResource(name);
        return url;
    }

    @Override
    public Enumeration getResources(String name) throws IOException {
        Enumeration urls = findResources(name);
        if (null != urls) {
            return urls;
        }
        urls = super.getResources(name);
        return urls;
    }

    @Override
    protected Class loadClass(final String javaClassName, final boolean resolve) throws ClassNotFoundException {
        // 优先查询类加载路由表,如果命中路由规则,则优先从路由表中的ClassLoader完成类加载
        if (ArrayUtils.isNotEmpty(routingArray)) {
            for (final Routing routing : routingArray) {
                if (!routing.isHit(javaClassName)) {
                    continue;
                }
                final ClassLoader routingClassLoader = routing.classLoader;
                try {
                    System.out.println("被转发的类名称:" + javaClassName);
                    return routingClassLoader.loadClass(javaClassName);
                } catch (Exception cause) {
                    // 如果在当前routingClassLoader中找不到应该优先加载的类(应该不可能,但不排除有就是故意命名成同名类)
                    // 此时应该忽略异常,继续往下加载
                    // ignore...
                }
            }
        }

        // 先走一次已加载类的缓存,如果没有命中,则继续往下加载
        final Class loadedClass = findLoadedClass(javaClassName);
        if (loadedClass != null) {
            return loadedClass;
        }

        try {
            Class aClass = findClass(javaClassName);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception cause) {
            System.out.println("================================" + javaClassName);
            return super.loadClass(javaClassName, resolve);
        }
    }

    
    public static class Routing {

        private final Collection regexExpresses = new ArrayList();
        private ClassLoader classLoader;

        
        public Routing(final ClassLoader classLoader, final String... regexExpressArray) {
            if (ArrayUtils.isNotEmpty(regexExpressArray)) {
                regexExpresses.addAll(Arrays.asList(regexExpressArray));
            }
            this.classLoader = classLoader;
        }

        
        private boolean isHit(final String javaClassName) {
            for (final String regexExpress : regexExpresses) {
                try {
                    if (javaClassName.matches(regexExpress)) {
                        return true;
                    }
                } catch (Throwable cause) {
                    cause.printStackTrace();
//                    logger.warn("routing {} failed, regex-express={}.", javaClassName, regexExpress, cause);
                }
            }
            return false;
        }

    }


    @Override
    protected void finalize() throws Throwable {
        // 一旦这个类被回收的话,会被回调。
        System.out.println("ManagerClassLoader 终于被回收了!");
        super.finalize();
    }
}
2. 构建测试

这个测试比较简单:

  • 构建一个Map来管理加载的类
  • 每次加载一个ClassLoader的时候,先清空上一个。

为了简单方便,管理器永远只有一个加载器。但是为了查看效果,你可以重复一直加载。

  • 控制台输入1 的时候会手动加载一个jar包中的类。2 卸载jar包中的类和加载器. 3 触发GC看是否会被回收掉。

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {
        File file = new File("E:\study\sandbox\sandbox-module\manager-plugins\cat-plugin-1.3.3-jar-with-dependencies.jar");
//        URL urls = new URL("file:C:/Users/liukx/AppData/Local/Temp/manager_plugin124980413499729388.jar");
        Map cacheMap = new HashMap<>();

        Scanner input = new Scanner(System.in);
        while (true) {
            System.out.println("请输入执行 [1 : 加载 , 3 : 卸载]");
            int next = input.nextInt();
            System.out.println("接收到的指令:" + next);

            if (1 == next) {
                // 先清除上一个加载器
                clearClassLoader(cacheMap);
                // 加载一个新的类加载器
                AnnotationConfigApplicationContext applicationContext = newManager(file);
                cacheMap.put("A", applicationContext);
            } else if (2 == next) {
                clearClassLoader(cacheMap);
            } else if (3 == next) {
                System.gc();
                System.out.println("触发了一次GC操作!");
            }
        }
    }
	
    // 先清空上一个加载器。
    private static void clearClassLoader(Map cacheMap) throws IOException {
        AnnotationConfigApplicationContext context = cacheMap.remove("A");
        Optional.ofNullable(context).ifPresent((c) -> {
            ManagerClassLoader classLoader = (ManagerClassLoader) c.getClassLoader();
            try {
                Objects.requireNonNull(classLoader).close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("清除缓存");
        });
    }
	
    // 实际中的自定义管理器
    private static AnnotationConfigApplicationContext newManager(File file) {
        List includeClass = new ArrayList<>();
        includeClass.add("^com\.sandbox\.manager\.api\..*");
        includeClass.add("^com\.alibaba\.jvm\.sandbox\.api\..*");
       //  includeClass.add("^com\.lkx\..*"); //todo 原来如此
//        // includeClass.add("^org\.apache\.commons\.lang3\..*");
        includeClass.add("^org\.springframework\..*");
//        includeClass.add("^java\..*");

        ManagerClassLoader urlClassLoader = new ManagerClassLoader(new URL[]{builderUrl(file)}, new ManagerClassLoader.Routing(
                ClassLoaderTest.class.getClassLoader(),
                includeClass.toArray(includeClass.toArray(new String[0]))));
        AnnotationConfigApplicationContext pluginApplicationContext = new AnnotationConfigApplicationContext();
        pluginApplicationContext.setClassLoader(urlClassLoader);
        pluginApplicationContext.scan("com.sandbox.application.plugin");
        pluginApplicationContext.refresh();

        Trace bean = pluginApplicationContext.getBean(Trace.class);
        String id = bean.getId();
        System.out.println(">>>>> 执行 :: " + id);
        return pluginApplicationContext;
    }
	// 简单的自定义加载方式
    private static AnnotationConfigApplicationContext newMyClassLoader(File file) {
        MyClassLoader urlClassLoader = new MyClassLoader(new URL[]{builderUrl(file)});
        AnnotationConfigApplicationContext pluginApplicationContext = new AnnotationConfigApplicationContext();
        pluginApplicationContext.setClassLoader(urlClassLoader);
        pluginApplicationContext.scan("com.sandbox.application.plugin");
        pluginApplicationContext.refresh();
        return pluginApplicationContext;
    }
	// 最简单的加载方式
    private static AnnotationConfigApplicationContext newURLClassloader(File file) {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{builderUrl(file)}, ClassLoaderTest.class.getClassLoader());
        AnnotationConfigApplicationContext pluginApplicationContext = new AnnotationConfigApplicationContext();
        pluginApplicationContext.setClassLoader(urlClassLoader);
        pluginApplicationContext.scan("com.sandbox.application.plugin");
        pluginApplicationContext.refresh();
        return pluginApplicationContext;
    }

    private static URL builderUrl(File file) {
        try {
            // 每次都是构建一个新的临时的jar
            File tempFile = File.createTempFile("manager_plugin", ".jar");
            tempFile.deleteOnExit();
            FileUtils.copyFile(file, tempFile);
            return new URL("file:" + tempFile.getPath());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

执行右键,运行main方法

  • 反复输入1 不断重复加载。

这个时候我用的是JProfile、其实还可以查看java自带的jvisualvm.exe工具查看。

这里还是稍微记录一下jvisualvm.exe的使用方式:

  • 位置是在C:Program FilesJavajdk1.8.0_261binjvisualvm.exe。可以根据自己的java安装环境去查找。
  1. 你运行了程序,直接点击jvisualvm.exe打开。

这个时候你会看到虚拟机的运行环境,但是这个时候我们需要看某个实例的运行个数时。最好是在运行java程序中加入
-Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=4444 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

开启一个可远程观测的端口。


这个时候,你基本上可以看到实例的加载情况,但是无法追查到引用数据。

3. 使用jprofile去追查

Jprofile 11的下载
纯干货:内存溢出通过Jprofile排查思路以及实践总结
有需要的先了解一下上面的排查文章。

1. 定位java应用程序


点击OK。这个时候虚拟机的信息基本上都展现出来了。

2. 查看存活的类


定位你需要关注的类

3. 选择你关注的类,并生成快照


这个时候基本上中和类的总数和大小引入眼帘。

4. 追踪这个类的引用类

右键你选择的类


这个时候,有多少个实例就会有多少条记录。

其实我们目前按照正常情况来讲,触发GC之后应该只剩一个。但是现在显然不是。

这种情况一定是该实例引用被外部持有,没有被释放掉,导致GC无法回收这个实例。

随便打开一个看看:

关键引用图

说实话,一开始真看不出啥,确实没啥经验,只能慢慢摸索呗~

没有思路,这时我们可以换种方式: 排除法

遇到不会的,先搭一个简单的demo,一步一步朝着我们实际的实现出发。

越简单的案例越能快速反应问题,复杂的东西导致的因素会很多。

  1. 先写了一个newURLClassloader 方法,从URLClassLoder出发,发现没问题,能被回收。
  2. 然后在手写了一个简单自定义的方法newMyClassLoader,发现也没问题。
public class MyClassLoader extends URLClassLoader {


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

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

    public MyClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

}

嗯,那一定就是实现的方式出了毛病。

  1. 然后从实现的ManagerClassLoader类中把实现方法loadClass给注释掉了,发现居然是OK的。

嗯,越来越近了。

细看了一下loadClass方法:

发现也没啥,就是特定的路径使用特定的类加载器加载。

protected Class loadClass(final String javaClassName, final boolean resolve) throws ClassNotFoundException {
        // 优先查询类加载路由表,如果命中路由规则,则优先从路由表中的ClassLoader完成类加载
        if (ArrayUtils.isNotEmpty(routingArray)) {
            for (final Routing routing : routingArray) {
                if (!routing.isHit(javaClassName)) {
                    continue;
                }
                final ClassLoader routingClassLoader = routing.classLoader;
                try {
                    System.out.println("被转发的类名称:" + javaClassName);
                    return routingClassLoader.loadClass(javaClassName);
                } catch (Exception cause) {
                    // 如果在当前routingClassLoader中找不到应该优先加载的类(应该不可能,但不排除有就是故意命名成同名类)
                    // 此时应该忽略异常,继续往下加载
                    // ignore...
                }
            }
        }

        // 先走一次已加载类的缓存,如果没有命中,则继续往下加载
        final Class loadedClass = findLoadedClass(javaClassName);
        if (loadedClass != null) {
            return loadedClass;
        }

        try {
            Class aClass = findClass(javaClassName);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception cause) {
            System.out.println("================================" + javaClassName);
            return super.loadClass(javaClassName, resolve);
        }
    }

应该就是使用方式的问题。

private static AnnotationConfigApplicationContext newManager(File file) {
        List includeClass = new ArrayList<>();
        includeClass.add("^com\.sandbox\.manager\.api\..*");
        includeClass.add("^com\.alibaba\.jvm\.sandbox\.api\..*");
       //  includeClass.add("^com\.lkx\..*"); //todo 原来如此
//        // includeClass.add("^org\.apache\.commons\.lang3\..*");
        includeClass.add("^org\.springframework\..*");
//        includeClass.add("^java\..*");

        ManagerClassLoader urlClassLoader = new ManagerClassLoader(new URL[]{builderUrl(file)}, new ManagerClassLoader.Routing(
                ClassLoaderTest.class.getClassLoader(),
                includeClass.toArray(includeClass.toArray(new String[0]))));
        AnnotationConfigApplicationContext pluginApplicationContext = new AnnotationConfigApplicationContext();
        pluginApplicationContext.setClassLoader(urlClassLoader);
        pluginApplicationContext.scan("com.sandbox.application.plugin");
        pluginApplicationContext.refresh();

        Trace bean = pluginApplicationContext.getBean(Trace.class);
        String id = bean.getId();
        System.out.println(">>>>> 执行 :: " + id);
        return pluginApplicationContext;
    }

这里的话就是遇到这些类的话使用主加载器去加载,否则使用自己的加载器。

然后联想到关键引用图中有一个,这里有点运气的因素。
这个属于主加载器也有的,但是没在转发中声明路径,然后加入了这个路径。

//加上这个
includeClass.add("^com\.lkx\..*"); //todo 原来如此

然后按照上述步骤重新测试,发现com.lkx.jvm.sandbox.core.classloader.ManagerClassLoader#finalize的方法被回调了,类也被回收了。

此时,脑瓜子依然嗡嗡作响~。。。

给个解释吧?我也不知道啊!睡服不了自己啊?

强装镇定…

按照正常来讲,A和B是两个不同的加载器,B负责加载插件范围内的实例,比如lang3的工具类,这个是不会和A的工具类起冲突的,因为是各自独立的。那么InterfaceProxyUtils这个工具类为什么不同呢?即便A和B都依赖这个工具类,也是各自独立的。为什么会有引用关系呢?

知道了结果,这个时候我们开始反推过程。

然后开始捣鼓JProfile,发现有个功能可以从实例一直往上查找直到GC ROOT ! 绝了~

  • 选中一个应该被回收的类



从这个路径中可以发现挺多问题的,原来这个类是被Spring持有的。从之前的图也能看出端倪…

4. 胡说八道

为什么Spring会持有呢?首先我们加载插件包的时候是用的Spring的scan方式扫描的包,但是我们先看一下入口类 AttributeMethods

// 省略大部分源码
final class AttributeMethods {
    // 静态缓存类,而且还是全局的
    private static final Map, AttributeMethods> cache =
			new ConcurrentReferenceHashMap<>();
    
    // 重点看是哪里调用了这个静态方法
    static AttributeMethods forAnnotationType(@Nullable Class annotationType) {
		if (annotationType == null) {
			return NONE;
		}
		return cache.computeIfAbsent(annotationType, AttributeMethods::compute);
	}
}

原来这里面是有一个保存属性结构的全局缓存工具类,一旦加载插件包中发现属性注解的时候都会先缓存起来。

调用入口在org.springframework.core.annotation.AnnotationTypeMapping#AnnotationTypeMapping中调用了AttributeMethods._forAnnotationType_(annotationType);

我们插件包中确实有一个类注解缓存比如:

interface IHttpServletRequest {

    @InterfaceProxyUtils.ProxyMethod(name = "getRemoteAddr")
    String getRemoteAddress();

}

Spring在解析的时候会把一些结构性的东西保存下来。

这个时候相当于B加载器的实例对象引用被A加载器的实例应用持有了,所以一直回收不了。但是如果在ManagerClassLoader声明这个类的路径就是由A加载,B去A里面找的话,就能够被回收。


以上兜兜转转终于定位到了,也是对JProfile有了更深一步的了解。
很多时候当你知识面不够广的时候,可以换一种思路去验证:

  • 比如排除法,先把复杂的东西简单化,一步一步验证。
  • 在无意中得到解决方法的时候,你不知道为什么会这样?
  • 此时再通过结果反推过程,得到最终的原因。

如果此时你正在观看这篇文章,不要纠结能不能解决你目前的问题,排查思路和工具的使用能够让你让你多一种解决方案。

不太喜欢贴大量代码,影响阅读,所以不要纠结代码。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/684691.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号