今天(2020-01-18)在编写Netty相关代码的时候,从Netty源码中的ThreadDeathWatcher和GlobalEventExecutor追溯到两个和线程上下文类加载器ContextClassLoader内存泄漏相关的Issue:
ThreadDeathWatcher causes custom classLoader script memory leaks
Ensure ThreadDeathWatcher and GlobalEventExecutor will not cause clas…
两个Issue分别是两位前辈在2017-12的时候提出的,描述的是同一类问题,最后被Netty的负责人采纳,并且修复了对应的问题从而关闭了Issue。这里基于这两个Issue描述的内容,对ContextClassLoader内存泄漏隐患做一次复盘。
ClassLoader相关的内容一个JVM实例(Java应用程序)里面的所有类都是通过ClassLoader加载的。
不同的ClassLoader在JVM中有不同的命名空间,一个类实例(Class)的唯一标识是全类名 + ClassLoader,也就是不同的ClassLoader加载同一个类文件,也会得到不相同的Class实例。
JVM不提供类卸载的功能,从目前参考到的资料来看,类卸载需要满足下面几点:
条件一:Class的所有实例不被强引用(不可达)。
条件二:Class本身不被强引用(不可达)。
条件三:加载该Class的ClassLoader实例不被强引用(不可达)。
有些场景下需要实现类的热部署和卸载,例如定义一个接口,然后由外部动态传入代码的实现。
这一点很常见,最典型的就是在线编程,代码传到服务端再进行编译和运行。
由于应用启动期所有非JDK类库的类都是由AppClassLoader加载,我们没有办法通过AppClassLoader去加载非类路径下的已存在同名的类文件(对于一个ClassLoader而言,每个类文件只能加载一次,生成唯一的Class),所以为了动态加载类,每次必须使用完全不同的自定义ClassLoader实例加载同一个类文件或者使用同一个自定义的ClassLoader实例加载不同的类文件。类的热部署这里举个简单例子:
// 此文件在项目类路径 package club.throwable.loader; public class DefaultHelloService implements HelloService { @Override public String sayHello() { return "default say hello!"; } } // 下面两个文件编译后放在I盘根目录 // I:\\DefaultHelloService1.class package club.throwable.loader; public class DefaultHelloService1 implements HelloService { @Override public String sayHello() { return "1 say hello!"; } } // I:\\DefaultHelloService2.class package club.throwable.loader; public class DefaultHelloService2 implements HelloService { @Override public String sayHello() { return "2 say hello!"; } } // 接口和运行方法 public interface HelloService { String sayHello(); static void main(String[] args) throws Exception { HelloService helloService = new DefaultHelloService(); System.out.println(helloService.sayHello()); ClassLoader loader = new ClassLoader() { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String location = "I:\\DefaultHelloService1.class"; if (name.contains("DefaultHelloService2")) { location = "I:\\DefaultHelloService2.class"; } File classFile = new File(location); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { InputStream stream = new FileInputStream(classFile); int b; while ((b = stream.read()) != -1) { outputStream.write(b); } } catch (IOException e) { throw new IllegalArgumentException(e); } byte[] bytes = outputStream.toByteArray(); return super.defineClass(name, bytes, 0, bytes.length); } }; Class<?> klass = loader.loadClass("club.throwable.loader.DefaultHelloService1"); helloService = (HelloService) klass.newInstance(); System.out.println(helloService.sayHello()); klass = loader.loadClass("club.throwable.loader.DefaultHelloService2"); helloService = (HelloService) klass.newInstance(); System.out.println(helloService.sayHello()); } } // 控制台输出 default say hello! 1 say hello! 2 say hello!如果新建过多的ClassLoader实例和Class实例,会占用大量的内存,如果由于上面几个条件无法全部满足,也就是这些ClassLoader实例和Class实例一直堆积无法卸载,那么就会导致内存泄漏(memory leak,后果很严重,有可能耗尽服务器的物理内存,因为JDK1.8+类相关元信息存在在元空间metaspace,而元空间使用的是native memory)。
线程中的ContextClassLoaderContextClassLoader其实指的是线程类java.lang.Thread中的contextClassLoader属性,它是ClassLoader类型,也就是类加载器实例。有些场景下,JDK提供了一些标准接口需要第三方提供商去实现(最常见的就是SPI,Service Provider Interface,例如java.sql.Driver),这些标准接口类是由启动类加载器(Bootstrap ClassLoader)加载,但是这些接口的实现类需要从外部引入,本身不属于JDK的原生类库,无法用启动类加载器加载。为了解决此困境,引入了线程上下文类加载器Thread Context ClassLoader。线程java.lang.Thread实例在初始化的时候会调用Thread#init()方法,Thread类和contextClassLoader相关的核心代码块如下:
// 线程实例的初始化方法,new Thread()的时候一定会调用 private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { // 省略其他代码 Thread parent = currentThread(); // 省略其他代码 if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; // 省略其他代码 } public void setContextClassLoader(ClassLoader cl) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("setContextClassLoader")); } contextClassLoader = cl; } @CallerSensitive public ClassLoader getContextClassLoader() { if (contextClassLoader == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { ClassLoader.checkClassLoaderPermission(contextClassLoader, Reflection.getCallerClass()); } return contextClassLoader; }首先明确两点:
Thread实例允许手动设置contextClassLoader属性,覆盖当前的线程上下文类加载器实例。