在最近一个项目中,在项目发布以后,发现系统中有内存泄漏问题。表象是堆内存随着系统的运行时间缓慢增加,一直没有办法经过gc来回收,最终于致使堆内存耗尽,内存溢出。开始是怀疑ThreadLocal的问题,由于在项目中,大量使用了线程的ThreadLocal保存线程上下文信息,在正常状况下,在线程开始的时候设置线程变量,在线程结束的时候,须要清除线程上下文信息,若是线程变量没有清除,会致使线程中保存的对象没法释放。java
从这个正常的状况来看,假设没有清除线程上下文变量,那么在线程结束的时候(线程销毁),线程上下文变量所占用的内存会随着线程的销毁而被回收。至少从程序设计者角度来看,应该如此。实际状况下是怎么样,须要进行测试。web
可是对于web类型的应用,为了不产生大量的线程产生堆栈溢出(默认状况下一个线程会分配512K的栈空间),都会采用线程池的设计方案,对大量请求进行负载均衡。因此实际应用中,通常都会是线程池的设计,处理业务的线程数通常都在200如下,即便全部的线程变量都没有清理,那么理论上会出现线程保持的变量最大数是200,若是线程变量所指示的对象占用比较少(小于10K),200个线程最多只有2M(200*10K)的内存没法进行回收(由于线程池线程是复用的,每次使用以前,都会重新设置新的线程变量,那么老的线程变量所指示的对象没有被任何对象引用,会自动被垃圾回收,只有最后一次线程被使用的状况下,才没法进行回收)。负载均衡
以上只是理论上的分析,那么实际状况下如何了,我写了一段代码进行实验。测试
处理器名称: Intel Core i7 2.3 GHz 4核this
内存: 16 GBspa
操做系统:OS X 10.8.2操作系统
java版本:"1.7.0_04-ea"线程
-Xms128M -Xmx512M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:gc.log设计
测试代码:Test.java 日志
import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int testCase= Integer.parseInt(br.readLine()); br.close(); switch(testCase){ // 测试状况1. 无线程池,线程不休眠,而且清除thread_local 里面的线程变量;测试结果:无内存溢出 case 1 :testWithThread(true, 0); break; // 测试状况2. 无线程池,线程不休眠,没有清除thread_local 里面的线程变量;测试结果:无内存溢出 case 2 :testWithThread(false, 0); break; // 测试状况3. 无线程池,线程休眠1000毫秒,清除thread_local里面的线程的线程变量;测试结果:无内存溢出,可是新生代内存总体使用高 case 3 :testWithThread(false, 1000); break; // 测试状况4. 无线程池,线程永久休眠(设置最大值),清除thread_local里面的线程的线程变量;测试结果:无内存溢出 case 4 :testWithThread(true, Integer.MAX_VALUE); break; // 测试状况5. 有线程池,线程池大小50,线程不休眠,而且清除thread_local 里面的线程变量;测试结果:无内存溢出 case 5 :testWithThreadPool(50,true,0); break; // 测试状况6. 有线程池,线程池大小50,线程不休眠,没有清除thread_local 里面的线程变量;测试结果:无内存溢出 case 6 :testWithThreadPool(50,false,0); break; // 测试状况7. 有线程池,线程池大小50,线程无限休眠,而且清除thread_local 里面的线程变量;测试结果:无内存溢出 case 7 :testWithThreadPool(50,true,Integer.MAX_VALUE); break; // 测试状况8. 有线程池,线程池大小1000,线程无限休眠,而且清除thread_local 里面的线程变量;测试结果:无内存溢出 case 8 :testWithThreadPool(1000,true,Integer.MAX_VALUE); break; default :break; } } public static void testWithThread(boolean clearThreadLocal, long sleepTime) { while (true) { try { Thread.sleep(100); new Thread(new TestTask(clearThreadLocal, sleepTime)).start(); } catch (Exception e) { e.printStackTrace(); } } } public static void testWithThreadPool(int poolSize,boolean clearThreadLocal, long sleepTime) { ExecutorService service = Executors.newFixedThreadPool(poolSize); while (true) { try { Thread.sleep(100); service.execute(new TestTask(clearThreadLocal, sleepTime)); } catch (Exception e) { e.printStackTrace(); } } } public static final byte[] allocateMem() { // 这里分配一个1M的对象 byte[] b = new byte[1024 * 1024]; return b; } static class TestTask implements Runnable { /** 是否清除上下文参数变量 */ private boolean clearThreadLocal; /** 线程休眠时间 */ private long sleepTime; public TestTask(boolean clearThreadLocal, long sleepTime) { this.clearThreadLocal = clearThreadLocal; this.sleepTime = sleepTime; } public void run() { try { ThreadLocalHolder.set(allocateMem()); try { // 大于0的时候才休眠,不然不休眠 if (sleepTime > 0) { Thread.sleep(sleepTime); } } catch (InterruptedException e) { } } finally { if (clearThreadLocal) { ThreadLocalHolder.clear(); } } } } }
ThreadLocalHolder.java
public class ThreadLocalHolder { public static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(); public static final void set(byte [] b){ threadLocal.set(b); } public static final void clear(){ threadLocal.set(null); } }
无线程池的状况:测试用例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为空的元素。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadLocalLeakTest { public static void main(String[] args) { // 若是控制线程池的大小为50,不会致使内存溢出 testWithThreadPool(50); // 也不会致使内存泄露 testWithThread(); } static class TestTask implements Runnable { public void run() { ThreadLocal tl = new ThreadLocal(); // 确保threadLocal为局部对象,在退出run方法以后,没有任何强引用,能够被垃圾回收 tl.set(allocateMem()); } } public static void testWithThreadPool(int poolSize) { ExecutorService service = Executors.newFixedThreadPool(poolSize); while (true) { try { Thread.sleep(100); service.execute(new TestTask()); } catch (Exception e) { e.printStackTrace(); } } } public static void testWithThread() { try { Thread.sleep(100); } catch (InterruptedException e) { } new Thread(new TestTask()).start(); } public static final byte[] allocateMem() { // 这里分配一个1M的对象 byte[] b = new byte[1024 * 1024 * 1]; return b; } }