实例详解Java中ThreadLocal内存泄露
案例与分析
问题背景
在 tomcat 中,下面的代码都在 webapp 内,会导致webappclassloader
泄漏,无法被回收。
public class mycounter { private int count = 0; public void increment() { count++; } public int getcount() { return count; } } public class mythreadlocal extends threadlocal<mycounter> { } public class leakingservlet extends httpservlet { private static mythreadlocal mythreadlocal = new mythreadlocal(); protected void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception { mycounter counter = mythreadlocal.get(); if (counter == null) { counter = new mycounter(); mythreadlocal.set(counter); } response.getwriter().println( "the current thread served this servlet " + counter.getcount() + " times"); counter.increment(); } }
上面的代码中,只要leakingservlet
被调用过一次,且执行它的线程没有停止,就会导致webappclassloader
泄漏。每次你 reload
一下应用,就会多一份webappclassloader
实例,最后导致 permgen outofmemoryexception
。
解决问题
现在我们来思考一下:为什么上面的threadlocal
子类会导致内存泄漏?
webappclassloader
首先,我们要搞清楚webappclassloader
是什么鬼?
对于运行在 java ee容器中的 web 应用来说,类加载器的实现方式与一般的 java 应用有所不同。不同的 web 容器的实现方式也会有所不同。以 apache tomcat 来说,每个 web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 java servlet 规范中的推荐做法,其目的是使得 web 应用自己的类的优先级高于 web 容器提供的类。这种代理模式的一个例外是:java 核心库的类是不在查找范围之内的。这也是为了保证 java 核心库的类型安全。
也就是说webappclassloader
是 tomcat 加载 webapp 的自定义类加载器,每个 webapp 的类加载器都是不一样的,这是为了隔离不同应用加载的类。
那么webappclassloader
的特性跟内存泄漏有什么关系呢?目前还看不出来,但是它的一个很重要的特点值得我们注意:每个 webapp 都会自己的webappclassloader
,这跟 java 核心的类加载器不一样。
我们知道:导致webappclassloader
泄漏必然是因为它被别的对象强引用了,那么我们可以尝试画出它们的引用关系图。等等!类加载器的作用到底是啥?为什么会被强引用?
类的生命周期与类加载器
要解决上面的问题,我们得去研究一下类的生命周期和类加载器的关系。
跟我们这个案例相关的主要是类的卸载:
在类使用完之后,如果满足下面的情况,类就会被卸载:
1、该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。
2、加载该类的classloader
已经被回收。
3、该类对应的java.lang.class
对象没有任何地方被引用,没有在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm 就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java 类的整个生命周期就结束了。
由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的class对象,因此这些class对象始终是可触及的。
由用户自定义的类加载器加载的类是可以被卸载的。
注意上面这句话,webappclassloader
如果泄漏了,意味着它加载的类都无法被卸载,这就解释了为什么上面的代码会导致 permgen outofmemoryexception
。
关键点看下面这幅图
我们可以发现:类加载器对象跟它加载的 class 对象是双向关联的。这意味着,class 对象可能就是强引用webappclassloader
,导致它泄漏的元凶。
引用关系图
理解类加载器与类的生命周期的关系之后,我们可以开始画引用关系图了。(图中的leakingservlet.class
与mythreadlocal
引用画的不严谨,主要是想表达mythreadlocal
是类变量的意思)
下面,我们根据上面的图来分析webappclassloader
泄漏的原因。
1、leakingservlet
持有static
的mythreadlocal
,导致mythreadlocal
的生命周期跟leakingservlet
类的生命周期一样长。意味着mythreadlocal
不会被回收,弱引用形同虚设,所以当前线程无法通过threadlocalmap
的防护措施清除counter的强引用。
2、强引用链:thread -> threadlocalmap -> counter -> mycounter.class -> webappclasslocader
,导致webappclassloader
泄漏。
总结
内存泄漏是很难发现的问题,往往由于多方面原因造成。threadlocal由于它与线程绑定的生命周期成为了内存泄漏的常客,稍有不慎就酿成大祸。本文只是对一个特定案例的分析,若能以此举一反三,那便是极好的。希望本文对大家能有所帮助。