前言

之前写了一篇深入分析 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是类变量的意思)

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

  1. LeakingServlet持有staticMyThreadLocal,导致myThreadLocal的生命周期跟LeakingServlet类的生命周期一样长。意味着myThreadLocal不会被回收,弱引用形同虚设,所以当前线程无法通过ThreadLocalMap的防护措施清除counter的强引用。
  2. 强引用链:thread -> threadLocalMap -> counter -> MyCounter.class -> WebappClassLocader,导致WebappClassLoader泄漏。

总结

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

本文只是对一个特定案例的分析,若能以此举一反三,那便是极好的。最后我留另一个类似的案例供读者分析。

课后题

假设我们有一个定义在 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();
}
}

提示

Java 中 ThreadLocal 内存泄露的实例分析的更多相关文章

  1. Java中的内存泄露 和 JVM GC(垃圾回收机制)

    一.什么是Java中的内存泄露? 在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点, 首先,这些对象是可达的,即在有向图中,存在通路可以与其相连:其次,这些对象是无用的,即程序以 ...

  2. Java中的内存泄露的几种可能

    Java内存泄漏引起的原因: 内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏. 长生命周期的对象持有短生命周期对象的引用就很可能发 ...

  3. 诊断Java中的内存泄露

    每次我怀疑有内存泄漏时,我都要翻箱倒柜找这些命令.所以,这里总结一下以备后用: 首先,我用下面的命令监视进程: 1 while ( sleep 1 ) ; do ps -p $PID -o %cpu, ...

  4. java中对于多态的一个实例分析

    首先来看这样的一段代码,其中对于类的定义如下: class Parent{ public int myValue=100; public void printValue() { System.out. ...

  5. Java 中的内存泄露

    1.当你完成对流的读写时,应该通过调同close方法来关闭它,这个调用会释放掉十分有限的系统资源,否则,如果一个应用程序打开了过多的流而没有关闭,那么系统资源将被耗尽.

  6. Java中的内存泄露

  7. Java中关于内存泄漏出现的原因以及如何避免内存泄漏

    转账自:http://blog.csdn.net/wtt945482445/article/details/52483944 Java 内存分配策略 Java 程序运行时的内存分配策略有三种,分别是静 ...

  8. java中的内存溢出和内存泄漏

    内存溢出:对于整个应用程序来说,JVM内存空间,已经没有多余的空间分配给新的对象.所以就发生内存溢出. 内存泄露:在应用的整个生命周期内,某个对象一直存在,且对象占用的内存空间越来越大,最终导致JVM ...

  9. 查找并修复Android中的内存泄露—OutOfMemoryError

    [编者按]本文作者为来自南非约翰内斯堡的女程序员 Rebecca Franks,Rebecca 热衷于安卓开发,拥有4年安卓应用开发经验.有点完美主义者,喜爱美食. 本文系国内ITOM管理平台 One ...

随机推荐

  1. cdn

    cdn内容分发网络: 1. 内容缓存Web cache技术,反向代理 2. 集群服务与负载均衡技术 LVS(四层) 与 nginx(七层) 3. 全局负载均衡工作原理:基于DNS解析的GSLB实现机制 ...

  2. mac系统下mysql开机启动总是3307

    修改了mysql的my.cnf可还是不行,启动后就是3307,必须关掉再启动. 觉得可能是mac系统在哪里写死了开机启动项. http://queforum.com/mysql/1012987-mys ...

  3. 封锁Skype的广告(非原创)

    这个我也忘记在哪看的了 记录一下 好早以前微软收购了Skype 然后Skype就出现广告了.... 好吧废话少说  打开 控制面板 -> 网络和Internet -> Internet选项 ...

  4. CSS3 动画

      通过 CSS3,我们能够创建动画,这可以在许多网页中取代动画图片.Flash 动画以及 JavaScript. CSS3 动画 CSS3 @keyframes 规则 如需在 CSS3 中创建动画, ...

  5. ov5640摄像头设备驱动

    http://www.cnblogs.com/firege/p/5806121.html  (驱动大神) http://blog.csdn.net/yanbixing123/article/detai ...

  6. Ubuntu14.04下解压rar压缩包

    下载安装rarlinux安装 rarlinux官方网站下载地址:http://www.rarsoft.com/download.htm 安装(以我下载的rarlinux-x64-5.1.1.tar.g ...

  7. shared jedis 在spring中的配置

    redis 属性文件配置: redis.host=xx.xx.xx.xx redis.port=6379 #redis.pass=xxxxx redis.maxIdle=10000 redis.max ...

  8. Intel Code Challenge Final Round (Div. 1 + Div. 2, Combined) B. Batch Sort

    链接 题意:输入n,m,表示一个n行m列的矩阵,每一行数字都是1-m,顺序可能是乱的,每一行可以交换任意2个数的位置,并且可以交换任意2列的所有数 问是否可以使每一行严格递增 思路:暴力枚举所有可能的 ...

  9. 用Struts2拦截器实现文件下载前的验证

    思想:用户登录后,将登录信息存储在session中,每次需要验证时,取出来验证 缺陷:没有实现多用户登录时的情况 实行步骤: 登录信息的存储: ActionContext actionContext ...

  10. *cf.4 贪心

    D. Kostya the Sculptor time limit per test 3 seconds memory limit per test 256 megabytes input stand ...