ThreadLocal 内存泄露的实例分析

前言

以前写了一篇深刻分析 ThreadLocal 内存泄漏问题是从理论上分析ThreadLocal的内存泄漏问题,这一篇文章咱们来分析一下实际的内存泄漏案例。分析问题的过程比结果更重要,理论结合实际才能完全分析出内存泄漏的缘由。html

案例与分析

问题背景

在 Tomcat 中,下面的代码都在 webapp 内,会致使WebappClassLoader泄漏,没法被回收。java

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 OutOfMemoryExceptionweb

解决问题

如今咱们来思考一下:为何上面的ThreadLocal子类会致使内存泄漏?apache

WebappClassLoader

首先,咱们要搞清楚WebappClassLoader是什么鬼?tomcat

对于运行在 Java EE容器中的 Web 应用来讲,类加载器的实现方式与通常的 Java 应用有所不一样。不一样的 Web 容器的实现方式也会有所不一样。以 Apache Tomcat 来讲,每一个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不一样的是它是首先尝试去加载某个类,若是找不到再代理给父类加载器。这与通常类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐作法,其目的是使得 Web 应用本身的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围以内的。这也是为了保证 Java 核心库的类型安全。安全

也就是说WebappClassLoader是 Tomcat 加载 webapp 的自定义类加载器,每一个 webapp 的类加载器都是不同的,这是为了隔离不一样应用加载的类。app

那么WebappClassLoader的特性跟内存泄漏有什么关系呢?目前还看不出来,可是它的一个很重要的特色值得咱们注意:每一个 webapp 都会本身的WebappClassLoader,这跟 Java 核心的类加载器不同。webapp

咱们知道:致使WebappClassLoader泄漏必然是由于它被别的对象强引用了,那么咱们能够尝试画出它们的引用关系图。等等!类加载器的做用究竟是啥?为何会被强引用?this

类的生命周期与类加载器

要解决上面的问题,咱们得去研究一下类的生命周期和类加载器的关系。这个问题提及来又是一篇文章,参考我作的笔记类的生命周期spa

跟咱们这个案例相关的主要是类的卸载:

在类使用完以后,若是知足下面的状况,类就会被卸载:

  1. 该类全部的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有任何地方被引用,没有在任何地方经过反射访问该类的方法。

若是以上三个条件所有知足,JVM 就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,Java 类的整个生命周期就结束了。

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机自己会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,所以这些Class对象始终是可触及的。

由用户自定义的类加载器加载的类是能够被卸载的。

注意上面这句话,WebappClassLoader若是泄漏了,意味着它加载的类都没法被卸载,这就解释了为何上面的代码会致使 PermGen OutOfMemoryException

关键点看下面这幅图

咱们能够发现:类加载器对象跟它加载的 Class 对象是双向关联的。这意味着,Class 对象可能就是强引用WebappClassLoader,致使它泄漏的元凶。

引用关系图

理解类加载器与类的生命周期的关系以后,咱们能够开始画引用关系图了。(图中的LeakingServlet.classmyThreadLocal引用画的不严谨,主要是想表达myThreadLocal是类变量的意思)
leak_1leak_1

下面,咱们根据上面的图来分析WebappClassLoader泄漏的缘由。

  1. LeakingServlet持有staticMyThreadLocal,致使myThreadLocal的生命周期跟LeakingServlet类的生命周期同样长。意味着myThreadLocal不会被回收,弱引用形同虚设,因此当前线程没法经过ThreadLocalMap的防御措施清除counter的强引用(见深刻分析 ThreadLocal 内存泄漏问题)。
  2. 强引用链:thread -> threadLocalMap -> counter -> MyCounter.class -> WebappClassLocader,致使WebappClassLoader泄漏。

总结

内存泄漏是很难发现的问题,每每因为多方面缘由形成。ThreadLocal因为它与线程绑定的生命周期成为了内存泄漏的常客,稍有不慎就酿成大祸。

本文只是对一个特定案例的分析,若能以此触类旁通,那即是极好的。最后我留另外一个相似的案例供读者分析。

本文的案例来自于 Tomcat 的 Wiki MemoryLeakProtection

课后题

假设咱们有一个定义在 Tomcat Common Classpath 下的类(例如说在 tomcat/lib 目录下)

public class ThreadScopedHolder {
    private final static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>();
    public static void saveInHolder(Object o) {
        threadLocal.set(o);
    }
    public static Object getFromHolder() {
        return threadLocal.get();
    }
}

两个在 webapp 的类:

public class MyCounter {
    private int count = 0;
    public void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

public class LeakingServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws ServletException, IOException {
        MyCounter counter = (MyCounter) ThreadScopedHolder.getFromHolder();
        if (counter == null) {
            counter = new MyCounter();
            ThreadScopedHolder.saveInHolder(counter);
        }
        response.getWriter().println(
                "The current thread served this servlet " + counter.getCount()
                        + " times");
        counter.increment();
    }
}

提示

leak_2leak_2

欢迎你们批评指正,留言交流。

相关文章
相关标签/搜索