实例详解Java中ThreadLocal内存泄露

 更新时间:2016年08月14日 10:59:16   投稿:daisy  
这一篇文章我们来分析一个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.classmyThreadLocal引用画的不严谨,主要是想表达myThreadLocal是类变量的意思)

下面,我们根据上面的图来分析WebappClassLoader泄漏的原因。

      1、LeakingServlet持有staticMyThreadLocal,导致myThreadLocal的生命周期跟LeakingServlet类的生命周期一样长。意味着myThreadLocal不会被回收,弱引用形同虚设,所以当前线程无法通过ThreadLocalMap的防护措施清除counter的强引用。

      2、强引用链:thread -> threadLocalMap -> counter -> MyCounter.class -> WebappClassLocader,导致WebappClassLoader泄漏。

总结

内存泄漏是很难发现的问题,往往由于多方面原因造成。ThreadLocal由于它与线程绑定的生命周期成为了内存泄漏的常客,稍有不慎就酿成大祸。本文只是对一个特定案例的分析,若能以此举一反三,那便是极好的。希望本文对大家能有所帮助。

相关文章

  • Java程序执行过程及内存机制详解

    Java程序执行过程及内存机制详解

    本讲将介绍Java代码是如何一步步运行起来的,还会介绍Java程序所占用的内存是被如何管理的:堆、栈和方法区都各自负责存储哪些内容,感兴趣的朋友跟随小编一起看看吧
    2020-12-12
  • Java时间转换成unix时间戳的方法

    Java时间转换成unix时间戳的方法

    这篇文章主要为大家详细介绍了Java时间转换成unix时间戳的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-12-12
  • Spring Boot 捕捉全局异常 统一返回值的问题

    Spring Boot 捕捉全局异常 统一返回值的问题

    这篇文章主要介绍了Spring Boot 捕捉全局异常 统一返回值,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决

    MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决

    这篇文章主要介绍了MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教。
    2022-01-01
  • java socket 详细介绍

    java socket 详细介绍

    本篇文章小编为大家介绍,java socket 详细介绍。需要的朋友参考下
    2013-04-04
  • 利用java操作Excel文件的方法

    利用java操作Excel文件的方法

    以下是对利用java操作Excel文件的方法进行了详细的介绍,需要的朋友可以过来参考下
    2013-09-09
  • IDEA+Maven创建Spring项目的实现步骤

    IDEA+Maven创建Spring项目的实现步骤

    这篇文章主要介绍了IDEA+Maven创建Spring项目的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • Spring Boot如何读取自定义外部属性详解

    Spring Boot如何读取自定义外部属性详解

    这篇文章主要给大家介绍了关于Spring Boot如何读取自定义外部属性的相关资料,这个功能实现介绍的很详细,需要的朋友可以参考下
    2021-05-05
  • Spring常用注解及http数据转换教程

    Spring常用注解及http数据转换教程

    这篇文章主要为大家介绍了Spring常用注解及http数据转换原理以及接收复杂嵌套对象参数与Http数据转换的原理,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-03-03
  • 关于Java中Bean的生命周期详解

    关于Java中Bean的生命周期详解

    这篇文章主要介绍了关于Java中Bean的生命周期详解,所谓的⽣命周期指的是⼀个对象从诞⽣到销毁的整个⽣命过程,我们把这个过程就叫做⼀个对象的⽣命周期,需要的朋友可以参考下
    2023-08-08

最新评论