ThreadLocal 内存泄露的实例分析

Easter79
• 阅读 538

前言

之前写了一篇深入分析 ThreadLocal 内存泄漏问题是从理论上分析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

关键点看下面这幅图

ThreadLocal 内存泄露的实例分析

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

引用关系图

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

ThreadLocal 内存泄露的实例分析

下面,我们根据上面的图来分析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();
        }
}

提示

ThreadLocal 内存泄露的实例分析

点赞
收藏
评论区
推荐文章
深入了解 JavaScript 内存泄漏
在任何语言开发的过程中,对于内存的管理都非常重要,JavaScript也不例外。但是如果我们对内存泄漏没有什么概念,就有可能因为内存泄漏,导致许多问题。了解内存泄漏,如何避免内存泄漏,都是不可缺少的。
Stella981 Stella981
2年前
Android Native 内存泄漏系统化解决方案
导读:C内存泄漏问题的分析、定位一直是Android平台上困扰开发人员的难题。因为地图渲染、导航等核心功能对性能要求很高,高德地图APP中存在大量的C代码。解决这个问题对于产品质量尤为重要和关键,高德技术团队在实践中形成了一套自己的解决方案。分析和定位内存泄漏问题的核心在于分配函数的统计和栈回溯。如果只知道内存分配点不知道调用栈会
Stella981 Stella981
2年前
Lottie内存泄漏问题的定位与分析
!(https://oscimg.oschina.net/oscnet/b20c743eca63172a6d1e8fc2d3a3cdb3107.png)点击“搜狗测试”可关注我们!【一、前言Lottie简介与使用】 1、Lottie简介Lottie是Air
Wesley13 Wesley13
2年前
Java ThreadLocal的内存泄漏问题
ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:\存储单个线程上下文信息。比如存储id等;\使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;\减少参数传递。比如做一个trace工具,能够输出工程从开始到结
Wesley13 Wesley13
2年前
Java运行状态分析2:获取线程状态及堆栈信息
Java运行状态分析2:线程状态及堆栈信息基本概念出现内存泄漏或者运行缓慢场景,有时候无法直接从业务日志看出问题时候,需要分析jvm内存和线程堆栈线程堆栈信息主要记录jvm线程在某时刻线程执行情况,分析线程状态可以跟踪到程序出问题的地方​内存堆栈信息主要记录jvm堆中在某时刻对象使用情况,
Wesley13 Wesley13
2年前
Java 内存泄漏
  Java的一个重要优点就是通过垃圾收集器(GarbageCollection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存。因此,很多程序员认为Java不存在内存泄漏问题,或者认为即使有内存泄漏也不是程序的责任,而是GC或JVM的问题。其实,这种想法是不正确的,因为Java也存在内存泄露,但它的表现与C不同。  随着越来越多的服
Stella981 Stella981
2年前
JScript的内存泄漏
当一个系统没有妥善地管理它的内存分配时,就被称为内存泄漏,内存泄漏是一个bug,这问题会导致性能下降和运行错误。微软的InternetExplorer有许多的泄漏问题(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fmsdn.microsoft.com%2Flibrary%2Fdef
Easter79 Easter79
2年前
ThreadLocal的内存泄露的原因分析以及如何避免
前言在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露、强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这样情况发生,增强系统的健壮性。内存泄露内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果
小尉迟 小尉迟
11个月前
Java 性能分析工具 JProfiler 注册码
JProfiler是一个Java分析器,可帮助开发人员分析其应用程序的性能。它可用于识别性能瓶颈、内存泄漏和其他可能影响应用程序性能的问题。JProfiler提供了许多用于分析Java应用程序的功能,包括CPU分析、内存分析、线程分析以及与Eclipse、
京东云开发者 京东云开发者
8个月前
如何正确使用 ThreadLocal,你真的用对了吗? | 京东云技术团队
本文主要从源码的角度解析了ThreadLocal,并分析了发生内存泄漏的原因及正确用法,最后对它的应用场景进行了简单介绍。
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k