控制台输出:
接收到事件,事件内容:加载类成功,类名:club.throwable.loader.DefaultHelloService1 1 say hello! 接收到事件,事件内容:加载类成功,类名:club.throwable.loader.DefaultHelloService2 2 say hello!打开VisualVM,Dump对应进程的内存快照,多执行几次GC,发现了所有动态类都没有被卸载(这里除非主动终止线程Y释放自定义ClassLoader,否则永远都不可能释放该强引用),验证了前面的结论。
当然,这里只是加载了两个动态类,如果在特殊场景之下,例如在线编码和运行代码,那么有可能极度频繁动态编译和动态类加载,如果出现了上面类似的内存泄漏,那么很容易导致服务器内存耗尽。
解决方案参考那两个Issue,解决方案(或者说预防手段)基本上有两个:
不需要使用自定义类加载器的线程(如事件派发线程等)优先初始化,那么一般它的线程上下文类加载器是应用类加载器。
新建后代线程的时候,手动覆盖它的线程上下文类加载器,参考Netty的做法,在线程初始化的时候做如下的操作:
// ThreadDeathWatcher || GlobalEventExecutor AccessController.doPrivileged(new PrivilegedAction<Void>() { @Override public Void run() { watcherThread.setContextClassLoader(null); return null; } }); 小结这篇文章算是近期研究得比较深入的一篇文章,ContextClassLoader内存泄漏的隐患归根到底是引用使用不当导致一些本来在方法栈退出之后需要释放的引用无法释放导致的。这种问题有些时候隐藏得很深,而一旦命中了同样的问题并且在并发的场景之下,那么内存泄漏的问题会恶化得十分快。这类问题归类为性能优化,而性能优化是十分大的专题,以后应该也会遇到类似的各类问题,这些经验希望能对未来产生正向的作用。
参考资料:
《深入理解Java虚拟机 - 3rd》
我的个人博客Throwable