在最近一个项目中,在项目发布之后,发现系统中有内存泄漏问题。表象是堆内存随着系统的运行时间缓慢增长,一直没有办法通过gc来回收,最终于导致堆内存耗尽,内存溢出。开始是怀疑ThreadLocal的问题,因为在项目中,大量使用了线程的ThreadLocal保存线程上下文信息,在正常情况下,在线程开始的时候设置线程变量,在线程结束的时候,需要清除线程上下文信息,如果线程变量没有清除,会导致线程中保存的对象无法释放。

从这个正常的情况来看,假设没有清除线程上下文变量,那么在线程结束的时候(线程销毁),线程上下文变量所占用的内存会随着线程的销毁而被回收。至少从程序设计者角度来看,应该如此。实际情况下是怎么样,需要进行测试。

但是对于web类型的应用,为了避免产生大量的线程产生堆栈溢出(默认情况下一个线程会分配512K的栈空间),都会采用线程池的设计方案,对大量请求进行负载均衡。所以实际应用中,一般都会是线程池的设计,处理业务的线程数一般都在200以下,即使所有的线程变量都没有清理,那么理论上会出现线程保持的变量最大数是200,如果线程变量所指示的对象占用比较少(小于10K),200个线程最多只有2M(200*10K)的内存无法进行回收(因为线程池线程是复用的,每次使用之前,都会从新设置新的线程变量,那么老的线程变量所指示的对象没有被任何对象引用,会自动被垃圾回收,只有最后一次线程被使用的情况下,才无法进行回收)。

以上只是理论上的分析,那么实际情况下如何了,我写了一段代码进行实验。

  • 硬件配置:

处理器名称: Intel Core i7 2.3 GHz  4核

内存: 16 GB

  • 软件配置

操作系统:OS X 10.8.2

java版本:"1.7.0_04-ea"

  • JVM配置

-Xms128M -Xmx512M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:gc.log

测试代码:Test.java

  1. import java.io.BufferedReader;
  2. import java.io.InputStreamReader;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5.  
  6. public class Test {
  7.  
  8. public static void main(String[] args) throws Exception {
  9.  
  10. BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  11. int testCase= Integer.parseInt(br.readLine());
  12. br.close();
  13.  
  14. switch(testCase){
  15. // 测试情况1. 无线程池,线程不休眠,并且清除thread_local 里面的线程变量;测试结果:无内存溢出
  16. case 1 :testWithThread(true, 0); break;
  17. // 测试情况2. 无线程池,线程不休眠,没有清除thread_local 里面的线程变量;测试结果:无内存溢出
  18. case 2 :testWithThread(false, 0); break;
  19. // 测试情况3. 无线程池,线程休眠1000毫秒,清除thread_local里面的线程的线程变量;测试结果:无内存溢出,但是新生代内存整体使用高
  20. case 3 :testWithThread(false, 1000); break;
  21. // 测试情况4. 无线程池,线程永久休眠(设置最大值),清除thread_local里面的线程的线程变量;测试结果:无内存溢出
  22. case 4 :testWithThread(true, Integer.MAX_VALUE); break;
  23. // 测试情况5. 有线程池,线程池大小50,线程不休眠,并且清除thread_local 里面的线程变量;测试结果:无内存溢出
  24. case 5 :testWithThreadPool(50,true,0); break;
  25. // 测试情况6. 有线程池,线程池大小50,线程不休眠,没有清除thread_local 里面的线程变量;测试结果:无内存溢出
  26. case 6 :testWithThreadPool(50,false,0); break;
  27. // 测试情况7. 有线程池,线程池大小50,线程无限休眠,并且清除thread_local 里面的线程变量;测试结果:无内存溢出
  28. case 7 :testWithThreadPool(50,true,Integer.MAX_VALUE); break;
  29. // 测试情况8. 有线程池,线程池大小1000,线程无限休眠,并且清除thread_local 里面的线程变量;测试结果:无内存溢出
  30. case 8 :testWithThreadPool(1000,true,Integer.MAX_VALUE); break;
  31.  
  32. default :break;
  33.  
  34. }
  35. }
  36.  
  37. public static void testWithThread(boolean clearThreadLocal, long sleepTime) {
  38.  
  39. while (true) {
  40. try {
  41. Thread.sleep(100);
  42. new Thread(new TestTask(clearThreadLocal, sleepTime)).start();
  43. } catch (Exception e) {
  44. e.printStackTrace();
  45. }
  46. }
  47. }
  48.  
  49. public static void testWithThreadPool(int poolSize,boolean clearThreadLocal, long sleepTime) {
  50.  
  51. ExecutorService service = Executors.newFixedThreadPool(poolSize);
  52. while (true) {
  53. try {
  54. Thread.sleep(100);
  55. service.execute(new TestTask(clearThreadLocal, sleepTime));
  56. } catch (Exception e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. }
  61.  
  62. public static final byte[] allocateMem() {
  63. // 这里分配一个1M的对象
  64. byte[] b = new byte[1024 * 1024];
  65. return b;
  66. }
  67.  
  68. static class TestTask implements Runnable {
  69.  
  70. /** 是否清除上下文参数变量 */
  71. private boolean clearThreadLocal;
  72. /** 线程休眠时间 */
  73. private long sleepTime;
  74.  
  75. public TestTask(boolean clearThreadLocal, long sleepTime) {
  76. this.clearThreadLocal = clearThreadLocal;
  77. this.sleepTime = sleepTime;
  78. }
  79.  
  80. public void run() {
  81. try {
  82. ThreadLocalHolder.set(allocateMem());
  83. try {
  84. // 大于0的时候才休眠,否则不休眠
  85. if (sleepTime > 0) {
  86. Thread.sleep(sleepTime);
  87. }
  88. } catch (InterruptedException e) {
  89.  
  90. }
  91. } finally {
  92. if (clearThreadLocal) {
  93. ThreadLocalHolder.clear();
  94. }
  95. }
  96. }
  97. }
  98.  
  99. }

ThreadLocalHolder.java

  1. public class ThreadLocalHolder {
  2.  
  3. public static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>();
  4.  
  5. public static final void set(byte [] b){
  6. threadLocal.set(b);
  7. }
  8.  
  9. public static final void clear(){
  10. threadLocal.set(null);
  11. }
  12. }
  • 测试结果分析:

无线程池的情况:测试用例1-4

下面是测试用例1 的垃圾回收日志

下面是测试用例2 的垃圾回收日志

对比分析测试用例1 和 测试用例2 的GC日志,发现基本上都差不多,说明是否清楚线程上下文变量不影响垃圾回收,对于无线程池的情况下,不会造成内存泄露

对于测试用例3,由于业务线程sleep 一秒钟,会导致业务系统中有产生大量的阻塞线程,理论上新生代内存会比较高,但是会保持到一定的范围,不会缓慢增长,导致内存溢出,通过分析了测试用例3的gc日志,发现符合理论上的分析,下面是测试用例3的垃圾回收日志

通过上述日志分析,发现老年代产生了一次垃圾回收,可能是开始大量线程休眠导致内存无法释放,这一部分线程持有的线程变量会在重新唤醒之后运行结束被回收,新生代的内存内存一直维持在4112K,也就是4个线程持有的线程变量。

对于测试用例4,由于线程一直sleep,无法对线程变量进行释放,导致了内存溢出。

有线程池的情况:测试用例5-8

对于测试用例5,开设了50个工作线程,每次使用线程完成之后,都会清除线程变量,垃圾回收日志和测试用例1以及测试用例2一样。

对于测试用例6,也开设了50个线程,但是使用完成之后,没有清除线程上下文,理论上会有50M内存无法进行回收,通过垃圾回收日志,符合我们的语气,下面是测试用例6的垃圾回收日志

通过日志分析,发现老年代回收比较频繁,主要是因为50个线程持有的50M空间一直无法彻底进行回收,而新生代空间不够(我们设置的是128M内存,新生代大概36M左右)。所有整体内存的使用量肯定一直在50M之上。

对于测试用例7,由于工作线程最多50个,即使线程一直休眠,再短时间内也不会导致内存溢出,长时间的情况下会出现内存溢出,这主要是因为任务队列空间没有限制,和有没有清除线程上下文变量没有关系,如果我们使用的有限队列,就不会出现这个问题。

对于测试用例8,由于工作线程有1000个,导致至少1000M的堆空间被使用,由于我们设置的最大堆是512M,导致结果溢出。系统的堆空间会从开始的128M逐步增长到512M,最后导致溢出,从gc日志来看,也符合理论上的判断。由于gc日志比较大,就不在贴出来了。

所以从上面的测试情况来看,线上上下文变量是否导致内存泄露,是需要区分情况的,如果线程变量所占的空间的比较小,小于10K,是不会出现内存泄露的,导致内存溢出的。如果线程变量所占的空间比较大,大于1M的情况下,出现的内存泄露和内存溢出的情况比较大。以上只是jdk1.7版本情况下的分析,个人认为jdk1.6版本的情况和1.7应该差不多,不会有太大的差别。

-----------------------下面是对ThreadLocal的分析-------------------------------------

对于ThreadLocal的概念,很多人都是比较模糊的,只知道是线程本地变量,而具体这个本地变量是什么含义,有什么作用,如何使用等很多java开发工程师都不知道如何进行使用。从JDK的对ThreadLocal的解释来看

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,

它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

ThreadLocal有一个ThreadLocalMap静态内部类,你可以简单理解为一个MAP,这个‘Map’为每个线程复制一个变量的‘拷贝’存储其中。每一个内部线程都有一个ThreadLocalMap对象。

当线程调用ThreadLocal.set(T object)方法设置变量时,首先获取当前线程引用,然后获取线程内部的ThreadLocalMap对象,设置map的key值为threadLocal对象,value为参数中的object。

当线程调用ThreadLocal.get()方法获取变量时,首先获取当前线程引用,以threadLocal对象为key去获取响应的ThreadLocalMap,如果此‘Map’不存在则初始化一个,否则返回其中的变量。

也就是说每个线程内部的 ThreadLocalMap对象中的key保存的threadLocal对象的引用,从ThreadLocalMap的源代码来看,对threadLocal的对象的引用是WeakReference,也就是弱引用。

下面一张图描述这三者的整体关系

对于一个正常的Map来说,我们一般会调用Map.clear方法来清空map,这样map里面的所有对象就会释放。调用map.remove(key)方法,会移除key对应的对象整个entry,这样key和value 就不会任何对象引用,被java虚拟机回收。

而Thread对象里面的ThreadLocalMap里面的key是ThreadLocal的对象的弱引用,如果ThreadLocal对象会回收,那么ThreadLocalMap就无法移除其对应的value,那么value对象就无法被回收,导致内存泄露。但是如果thread运行结束,整个线程对象被回收,那么value所引用的对象也就会被垃圾回收。

什么情况下 ThreadLocal对象会被回收了,典型的就是ThreadLocal对象作为局部对象来使用或者每次使用的时候都new了一个对象。所以一般情况下,ThreadLocal对象都是static的,确保不会被垃圾回收以及任何时候线程都能够访问到这个对象。

写了下面一段代码进行测试,发现两个方法都没有导致内存溢出,对于没有使用线程池的方法来说,因为每次线程运行完就退出了,Map里面引用的所有对象都会被垃圾回收,所以没有关系,但是为什么线程池的方案也没有导致内存溢出了,主要原因是ThreadLocal.set方法的实现,会做一个将Key== null 的元素清理掉的工作。导致线程之前由于ThreadLocal对象回收之后,ThreadLocalMap中的value 也会被回收,可见设计者也注意到这个地方可能出现内存泄露,为了防止这种情况发生,从而清空ThreadLocalMap中null为空的元素。

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3.  
  4. public class ThreadLocalLeakTest {
  5.  
  6. public static void main(String[] args) {
  7. // 如果控制线程池的大小为50,不会导致内存溢出
  8. testWithThreadPool(50);
  9. // 也不会导致内存泄露
  10. testWithThread();
  11. }
  12.  
  13. static class TestTask implements Runnable {
  14.  
  15. public void run() {
  16. ThreadLocal tl = new ThreadLocal();
  17. // 确保threadLocal为局部对象,在退出run方法之后,没有任何强引用,可以被垃圾回收
  18. tl.set(allocateMem());
  19. }
  20. }
  21.  
  22. public static void testWithThreadPool(int poolSize) {
  23. ExecutorService service = Executors.newFixedThreadPool(poolSize);
  24. while (true) {
  25. try {
  26. Thread.sleep(100);
  27. service.execute(new TestTask());
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33.  
  34. public static void testWithThread() {
  35.  
  36. try {
  37. Thread.sleep(100);
  38. } catch (InterruptedException e) {
  39.  
  40. }
  41. new Thread(new TestTask()).start();
  42.  
  43. }
  44.  
  45. public static final byte[] allocateMem() {
  46. // 这里分配一个1M的对象
  47. byte[] b = new byte[1024 * 1024 * 1];
  48. return b;
  49. }
  50.  
  51. }

ThreaLocal内存泄露的问题的更多相关文章

  1. java: web应用中不经意的内存泄露

    前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下: 1.定义一个类App package com.cnblogs. ...

  2. 查看w3wp进程占用的内存及.NET内存泄露,死锁分析

    一 基础知识 在分析之前,先上一张图: 从上面可以看到,这个w3wp进程占用了376M内存,启动了54个线程. 在使用windbg查看之前,看到的进程含有 *32 字样,意思是在64位机器上已32位方 ...

  3. C++11 shared_ptr 智能指针 的使用,避免内存泄露

    多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...

  4. 基于HTML5的WebGL应用内存泄露分析

    上篇(http://www.hightopo.com/blog/194.html)我们通过定制了CPU和内存展示界面,体验了HT for Web通过定义矢量实现图形绘制与业务数据的代码解耦及绑定联动, ...

  5. android:布局、绘制、内存泄露、响应速度、listview和bitmap、线程优化以及一些优化的建议!

    1.布局优化 首先删除布局中无用的控件和层级,其次有选择地使用性能较低的viewgroup,比如布局中既可以使用RelativeLayout和LinearLayout,那我们就采用LinearLayo ...

  6. js内存泄露的几种情况详细探讨

    内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束.在C++中,因为是手动管理内存,内存泄露是经常出现的事情.而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使 ...

  7. 使用Xcode7的Instruments检测解决iOS内存泄露

    文/笨笨的糯糯(简书作者)原文链接:http://www.jianshu.com/p/0837331875f0著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 作为一名iOS开发攻城狮, ...

  8. 使用Visual Leak Detector for Visual C++ 捕捉内存泄露

    什么是内存泄漏? 内存泄漏(memory leak),指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况.内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段 ...

  9. java内存溢出和内存泄露

    虽然jvm可以通过GC自动回收无用的内存,但是代码不好的话仍然存在内存溢出的风险. 最近在网上搜集了一些资料,现整理如下: —————————————————————————————————————— ...

随机推荐

  1. HDU 5945 / BestCoder Round #89 1002 Fxx and game 单调队列优化DP

    Fxx and game 问题描述   青年理论计算机科学家Fxx给的学生设计了一款数字游戏. 一开始你将会得到一个数\:XX,每次游戏将给定两个参数\:k,tk,t, 任意时刻你可以对你的数执行下面 ...

  2. Tray - a SharedPreferences replacement for Android

    一个代替SharedPreferences的开源库, no Editor, no commit() no apply(),因此不存在UI卡顿现象,并且支持多线程,在一个线程中存另一个线程中取数据. h ...

  3. java中被各种XXUtil/XXUtils辅助类恶心到了,推荐这种命名方法

    且看一下有多少个StringUtils 列举一下XXUtil/XXUtils恶劣之处 1. 不知道该用XXUtil还是用XXUtils, 或者XXHelper, XXTool 2. 不知道该用a.ja ...

  4. 小tips

    ios::sync_with_stdio(false);  加速读入的,加上这条语句可以使cin和cout的速度和scanf和printf差不多.

  5. webpack2新特性

    增加 import() 作为代码分割点:System.import已被弃用,在webpack3时会被完全移除: 内置了json加载器,不再需要单独配置了 当打包文件过大时会提示性能警告,可以用 per ...

  6. three.js立方体

    <!DOCTYPE html> <html lang="en"> <head> <title>three.js webgl - ge ...

  7. 半吊子学习Swift--天气预报程序-准备工作

    MacBookPro买完快半年了,当初想着买个本本学点ios,买完就看了几天的教程[捂脸],最近发现人都要废了,想重新开始学习Swift并将每天的进程通过博客发布来督促自己. 由于文笔不好,接触Swi ...

  8. hello!

    今天是个星期天 第一次开通了朕的博客 么么哒 感觉很困 唔~晚安zzzzz

  9. Java_位运算(移位、位与、或、异或、非)

    public class Test { public static void main(String[] args) { // 1.左移( << ) // 0000 0000 0000 0 ...

  10. Linux Shell 重定向与管道【转帖】

    by 程默 在了解重定向之前,我们先来看看linux 的文件描述符. linux文件描述符:可以理解为linux跟踪打开文件,而分配的一个数字,这个数字有点类似c语言操作文件时候的句柄,通过句柄就可以 ...