(转)ThreadLocal的内存泄漏问题

标签: threadlocal 内存泄漏 问题 | 发表时间:2014-05-07 10:37 | 作者:wangzhangxing
出处:http://www.iteye.com

原文:http://www.godiscoder.com/?p=479

在最近一个项目中,在项目发布之后,发现系统中有内存泄漏问题。表象是堆内存随着系统的运行时间缓慢增长,一直没有办法通过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 

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;
    }

}


已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [threadlocal 内存泄漏 问题] 推荐:

(转)ThreadLocal的内存泄漏问题

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

(转)Java中字符串与内存泄漏的问题

- - jackyrong
对于这个写法,实际上对于oldStr是一个char[]数组[h,e,l,l,0,,,c,l,a,r,k],对于subString操作,newStr并不是自己copy oldStr的char[]数组hello自己去创建一个新的char[]数组,而是java在背后进行了String Reusing Optimization,它不会自己创建一个新的char数组,而是reuse原来的char数组.

JAVA内存泄漏问题处理方法经验总结

- - 编程语言 - ITeye博客
JVM问题,一般会有三种情况,目前遇到了两种,线程溢出和JVM不够用. 1.线程溢出:unable to create new native thread. 系统在1月4号左右,突然发现会产生内存溢出问题,从日志上看,错误信息为:. 导致系统不能使用,对外不能相应,但是观察gc等又处于正常情况,free 系统内存也正常.

常见的八种导致 APP 内存泄漏的问题

- - ITeye资讯频道
本文来自: http://blog.nimbledroid.com. 像Java这样具有垃圾回收功能的语言的好处之一,就是程序员无需手动管理内存分配. 这减少了段错误(segmentation fault)导致的闪退,也减少了内存泄漏导致的堆空间膨胀,让编写的代码更加安全. 然而,Java 中依然有可能发生内存泄漏.

内存泄漏

- - CSDN博客系统运维推荐文章
程序申请了堆空间,但是“忘记”释放,导致该块区域在程序结束前无法被再次使用导致的. 泄漏时间长了,就会导致用户空间内存不足,严重的导致死机. 如果泄漏比较严重,很容易察觉;但是有些泄漏很缓慢,不容易察觉,但是软件会运行很长时间后,会慢慢导致严重问题,而且当发现症状的时候,基本上已经是比较晚的时候了,想要识别泄漏,还是可以实现的,本篇文章来聊聊内存操作的原理.

ThreadLocal介绍

- - ITeye博客
一、java.lang.ThreadLocal. 一个实例就是一个容器,所有可以访问到这个实例的线程都可以在这个容器中存储一个该线程独立使用的变量. 这个实例里面其实是一个Map结构的属性,存储以线程对象为KEY,变量为VALUE的数据. 二、ThreadLocal有这样几个方法:. 返回当前线程对应的那个变量.

java内存泄漏

- - 编程语言 - ITeye博客
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址. Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的. GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题.

正确理解ThreadLocal

- - Java - 编程语言 - ITeye博客
转自: http://www.iteye.com/topic/103804. 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的. 另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本.

浅谈Java--内存泄漏

- - ITeye博客
      JAVA的垃圾回收机制,让许多程序员觉得内存管理不是很重要,但是内存内存泄露的事情恰恰这样的疏忽而发生,特别是对于Android开发,内存管理更为重要,养成良好的习惯,有利于避免内存的泄漏..     这里可以把许多对象和引用看成是有向图,顶点可以是对象也可以是引用,引用关系就是有向边.

Android 解析内存泄漏

- - CSDN博客移动开发推荐文章
1、引用没释放造成的内存泄露.        1.1、注册没取消造成的内存泄露.        这种 Android的内存泄露比纯 Java的内存泄露还要严重,因为其他一些Android程序可能引用我们的Anroid程序的对象(比如注册机制). 即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄露的内存依然不能被垃圾回收.