ThreadLocal引起的内存泄漏问题

在讲解ThreadLocal内存泄漏问题之前首先需要明确两个概念:

内存泄漏

指由于对象永远无法被垃圾回收导致其占用的JVM内存无法被释放。持续的内存泄漏会导致JVM可用内存减少,并最终可能导致JVM内存溢出(out of memory)直到JVM宕机。

伪内存泄漏

类似于内存泄漏,伪内存泄漏中对象所占用的内存在其不再使用后的相当长一段时间仍然无法被回收,也可能永远无法被回收。也就是说,伪内存泄漏中对象占用的内存看见可能被回收,也可能永远无法被回收(此时就是内存泄漏)

从ThreadLocal原理谈内存泄漏问题

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
这里写图片描述
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
上面的情况是理论上确实会出现的。但是我个人更想从更一般的情况来谈谈关于ThreadLocal内存泄漏问题:
很多时候可以发现发现ThreadLocal都是这样使用:

private static final ThreadLocal<Object> = new ThreadLocal<Object>();

在这种情况ThreadLocal基本上除了弱引用还有强引用,并且与所属类的生命周期同长。但有一点要清楚的是,Thread只是作为Key,它所需要的内存其实不多,真正的对象都在每个线程自己的ThreadLocalMap里,如果正确的编程,当线程结束的时候,ThreadLocalMap的对象会被释放。只要没有意外的让ThreadLocalMap里的变量逸出,很多时候是不需要考虑ThreadLocal所引起的内存泄漏问题。

更想谈谈ClassLoader引起的内存泄漏

先决条件:由 JVM 自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。JVM 自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。JVM 本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的 Class 对象,因此这些 Class 对象始终是可触及的

假设有A和B两个classloader,分别加载 A 和 B两个类。 他们new出来的instance,取名叫 a 和 b。此时,如果你通过某种方式,将a的instance传递给b,b又不小心hold住a的实例时,就有可能发生memory leak。 仔细分析,主要是因为a实例hold class A , class A hold classloader A,b hold a,那么b也就跟classload A有了间接引用关系。 GC过程中,如果发现classloader a 从classloader b的引用关系可达(reached),那么classloader A是不会被回收,这个时候如果ClassLoaderb的生命周期非常长,就会导致ClassLoaderA无法被回收,其加载的类也无法被卸载。
看下面一个场景:

public class LeakServlet extends HttpServlet {
    private static final String STATICNAME = "This leaks!";
    private static final Level CUSTOMLEVEL = new Level("level", 550) {
     }; //匿名内部类
     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
     Logger.getLogger("test").log(CUSTOMLEVEL, "doGet called");
 }
}
//Level的构造函数
protected Level(String name, int value, String resourceBundleName) {
  if (name == null) {
      throw new NullPointerException();
  }
  this.name = name;this.value = value;this.resourceBundleName = resourceBundleName;
   synchronized (Level.class) {
       known.add(this);//level.class拥有其所有的实例
    }
}

引用关系图:
这里写图片描述

由代码和引用关系图可以很清晰看到,Level.class作为JDK标准库的class,其与其生命周期与JVM等长,而且level.class拥有其所有的实例,CUSTOMLEVEL拥有内部类引用,内部类与WebAPPClassLoader相互关联,因此JDK标准类加载器引用了WebClassLoader导致WebClassLoader无法被虚拟机回收。这个时候如果经常reload引用就会导致创建多个WebClassLoader,而之前的WebClassLoader以及其加载的类始终得不到释放,导致JVM内存不断增大,多启动几次系统就报内存溢出的错误。(热部署次数过多出现内存泄漏也可以从这个方向排查)

如何在开发中避免ClassLoader泄漏

1.谨慎使用内部类,特别是对底层不熟悉的,内部类也很容易造成this逸出
2.使用JDK库的时候要避免让JDK的ClassLoader引用WebClassLoader

ClassLoader泄漏解决方法

1.从tomcat6.0.25开始,就有了”Find Leaks”按钮,用于检测应用是否存在classloader泄漏的功能。首先在tomcat8下启动应用,然后通过控制台关闭应用,然后查看控制台日志,如果日志信息提示某个线程未关闭,或者ThreadLocal绑定的实现未释放,就说明应用存在classloader内存泄漏问题,然后逐一解决问题
2.可以借助Mattias Jiderhamn大神开发的”classloader-leak-prevention”来解决这些问题。首先添加项目依赖

<dependency>
<groupId>se.jiderhamn.classloader-leak-prevention</groupId>
<artifactId>classloader-leak-prevention-servlet</artifactId>
<version>2.1.0</version>
</dependency>

web.xml中定义ClassLoaderLeakPreventorListener监听器

<listener>
<listener-class>se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorListener</listener-class>
</listener>

点击”Find Leaks”按钮,message提示“No web applications appear to have triggered a memory leak on stop, reload or undeploy.”说明应用不存在classloader内存泄漏问题,如果不是上面的提示信息,可以通过jconsole或者jvisualvm主动触发垃圾回收,然后再点击”Find Leaks”按钮,查看message提示信息。


ClassLoader内存泄漏参考:
http://www.tinygroup.org/blog/detail/5e78ace84a594e97905c2b2c44ee97f6
https://blog.csdn.net/jerome_s/article/details/52080261
ThreadLocal内存泄漏参考:
http://www.importnew.com/22039.html
《Java多线程编程实战指南 设计模式篇》

猜你喜欢

转载自blog.csdn.net/chenbinkria/article/details/79951792