前言

之前写了一篇深入分析 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

关键点看下面这幅图

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

引用关系图

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

欢迎大家批评指正,留言交流。

参考文章
ClassLoader内存溢出-从tomcat的reload说起
类加载器内存泄露与tomcat自定义加载器
类的生命周期
深入分析 ThreadLocal 内存泄漏问题
Tomcat源码解读系列(四)——Tomcat类加载机制概述
MemoryLeakProtection

ThreadLocal 内存泄露的实例分析相关推荐

  1. threadlocal内存泄露_ThreadLocal 简介

    本文转载于SegmentFault社区 作者:莫小点还有救 1. ThreadLocal简介 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的.如果想实现每一个线程都有自己的专属本地变量该如 ...

  2. java thread 内存泄露_Java ThreadLocal 内存泄露问题分析及解决方法。

    前言 在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露.强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这 ...

  3. threadlocal内存泄露_ThreadLocal原理解析

    谈一谈不常见却又不可少的ThreadLocal 在写ThreadLocal之前,需要先巩固下一点相关知识:Java内存模型及共享变量的可见性. 内存模型中所有变量存储在主内存中,当一个线程中要使用某个 ...

  4. 记录一次生产环境下的jvm内存泄露问题和分析解决过程!

    作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人! 发现异常 首先通过我们内部搭建的日志平台发现我们线上环境一个java应用有大量的http接口请求超时,登录lin ...

  5. Activity内部Handler引起内存泄露的原因分析

    有时在Activity中使用Handler时会提示一个内存泄漏的警告,代码通常如下: [java] view plaincopyprint? public class MainActivity ext ...

  6. threadlocal内存泄露_深入理解 ThreadLocal

    前言 上篇文章 https://juejin.im/post/5d712cedf265da03ea5a9ecf 中提到了获取线程的 Looper 是通过 ThreadLocal 来实现的: publi ...

  7. threadlocal内存泄露_ThreadLocal用法详解和原理

    一.用法 ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量. 1.Thr ...

  8. 内存泄露检测详细分析

    详细分析内存泄露检测 一般我们常说的内存泄漏是指堆内存的泄漏.堆内存是指程序从堆中分配的,使用完后必须显式释放的内存.C++中使用new和new[]实现从堆中分配到一块内存,使用完后,程序必须负责相应 ...

  9. 我们有一个线上的项目,刚启动完就占用了超过 1.5G,一次大量 JVM Native 内存泄露的排查分析(64M 问题)

    我们有一个线上的项目,刚启动完就占用了使用 top 命令查看 RES 占用了超过 1.5G,这明显不合理,于是进行了一些分析找到了根本的原因,下面是完整的分析过程,希望对你有所帮助. 会涉及到下面这些 ...

最新文章

  1. oracle闪回 分区,Oracle 闪回区(Oracle Flash recovery area)
  2. 【运筹学】表上作业法 ( 示例 | 使用 “ 最小元素法 “ 找初始基可行解 )
  3. 将mysql日期格式转换_如何将日期时间格式转换为mysql日期格式?
  4. 当社恐和社恐相亲时,场面会有多尴尬?
  5. 浏览器下载文件时一共发送2次请求,如何把“下载次数”只记录为1次?
  6. 从M个数中随机等可能的取出N个的问题
  7. Flask项目之手机端租房网站的实战开发(十三)
  8. 15. PHP 全局变量 - 超全局变量
  9. 2009福州数学建模题目及答案
  10. LM2596电源模块原理图及PCB分享
  11. Word文档没保存电脑死机了,重启打开文档一片空白怎么办?
  12. UTM(Urchin Tracking Module)简介
  13. 好书推荐-——《态度》——吴军老师著
  14. 资深程序员给Java初学者的学习路线建议
  15. 视频去水印的Python代码
  16. 图解ARP协议(三)ARP防御篇-如何揪出“内鬼”并“优雅的还手”
  17. js整数向上取整(自定义取整几位)
  18. MLAT-Autoencoders---下篇-关键代码及结果展示(3)(终)
  19. 对话 MySQL 之父 Monty:超越 MySQL 很难,但我做到了!
  20. 【TypeScript】TypeScript进阶

热门文章

  1. 2d的公式_西师大版六年级数学上册全册必背公式+高清版电子课文,收藏预习
  2. Servlet之间的跳转
  3. Java ExecutorService 线程池
  4. 华硕vm510l拆电池图解_图解说设备:凯斯CX80C你会买吗?
  5. 大数据python试卷_大数据起步--Python语言-中国大学mooc-试题题目及答案
  6. java date不要秒_java – 比较日期忽略Joda中DateTime的秒和毫秒时刻
  7. react 原生html 插件,纯原生JS的瀑布流插件Macy.js,前端必备插件
  8. 字节跳动测试开发4轮面试_字节跳动2018招聘测试开发方向(第四批)
  9. 小爱音箱怎么装app_79元的Redmi小爱音箱怎么样?这里有一份体验报告
  10. java stream 多次读取_多次从具有大量数据的Java InputStream中读取