threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法.

  在threadlocal的生命周期中,都存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.

  

  每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

  所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。  

  PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里的key为null的value,但这不是清除所有的key(只有在get set遇到key为null的entry时才会开始清除对应的value,而且不是全部扫描,只是逐个向后扫描直到遇到null的entry为止。)。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

最近看到网上的一篇文章,分析说明ThreadLocal是如何内存泄露的. 但我不这么认为. ThreadLocal设计的很好,根本不存在内存泄露问题. 本文就结合图和代码的例子来验证我的看法.

网上的代码例子普遍是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadLocal tl = new MyThreadLocal();
        tl.set(new My50MB());
         
        tl=null;
         
        System.out.println("Full GC");
        System.gc();
    }
     
    public static class MyThreadLocal extends ThreadLocal {
        private byte[] a = new byte[1024*1024*1];
         
        @Override
        public void finalize() {
            System.out.println("My threadlocal 1 MB finalized.");
        }
    }
     
    public static class My50MB {
        private byte[] a = new byte[1024*1024*50];
         
        @Override
        public void finalize() {
            System.out.println("My 50 MB finalized.");
        }
    }
 
}

结果自然打印

Full GC
My threadlocal 1 MB finalized.

Thread.sleep 1秒是为了给GC一个反应的时间. GC优先级低,即使调用了system.gc也不能立刻执行.所以sleep 1秒.

很多人就开始分析了: threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法.

说的也比较正确,当value不再使用的时候,调用remove的确是很好的做法.但内存泄露一说却不正确. 这是threadlocal的设计的不得已而为之的问题.

首先,让我们看看在threadlocal的生命周期中,都存在哪些引用吧. 看下图: 实线代表强引用,虚线代表弱引用.

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 像上面code中的例子,当把threadlocal实例tl置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

从中可以看出,弱引用只存在于key上,所以key会被回收. 而value还存在着强引用.只有thead退出以后,value的强引用链条才会断掉. 看下面改进后的例子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Test2 {
 
    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
 
            @Override
            public void run() {
                ThreadLocal tl = new MyThreadLocal();
                tl.set(new My50MB());
                 
                tl=null;
                 
                System.out.println("Full GC");
                System.gc();
                 
            }
             
        }).start();
         
         
        System.gc();
        Thread.sleep(1000);
        System.gc();
        Thread.sleep(1000);
        System.gc();
        Thread.sleep(1000);
 
    }
 
}

这一次的打印将输出:

Full GC
My threadlocal 1 MB finalized.
My 50 MB finalized.

我们可以看到,所有的都回收了.为什么要多次调用system.gc()? 这和finalize方法的策略有关系. finalize是一个特别低优先级的线程,当执行gc时,如果一个对象需要被回收,先执行它的finalize方法.这意味着,本次gc可能无法真正回收这个具有finalize方法的对象.留待下次回收. 这里多次调用system.gc正是为了给finalize留些时间.

从上面的例子可以看出,当线程退出以后,我们的value被回收了. 这是正确的.这说明内存并没有泄露. 栈中还存在着对value的强引用路线.只是由于thread没有提供public接口,无法访问此value,但我们可以使用反射拿到这个value.

这也是不得已而为之的设计吧. 总之,如果不想依赖线程的生命周期,那就调用remove方法来释放value的内存吧. 让我们好好思考一下,有什么办法可以在tl=null的时候,也释放value呢?

ThreadLocal就隐含了生命周期绑定到PCB(线程控制块)的意思。自然,对于大部分只考虑GC是不是能即时回收的内存泄露定义来说,是可能存在泄露的。
但是这就是ThreadLocal的本意。

深入ThreadLocal之三(ThreadLocal可能引起的内存泄露)的更多相关文章

  1. ThreadLocal源码原理以及防止内存泄露

    ThreadLocal的原理图: 在线程任务Runnable中,使用一个及其以上ThreadLocal对象保存多个线程的一个及其以上私有值,即一个ThreadLocal对象可以保存多个线程一个私有值. ...

  2. Java 中 ThreadLocal 内存泄露的实例分析

    前言 之前写了一篇深入分析 ThreadLocal 内存泄漏问题是从理论上分析ThreadLocal的内存泄漏问题,这一篇文章我们来分析一下实际的内存泄漏案例.分析问题的过程比结果更重要,理论结合实际 ...

  3. 【转载】Java中如何写一段内存泄露的程序 & ThreadLocal 介绍和使用

    可以参考这段文章: link A1:通过以下步骤可以很容易产生内存泄露(程序代码不能访问到某些对象,但是它们仍然保存在内存中): 上文中提到了使用ThreadLocal造成了内存泄露,但是写的不清不楚 ...

  4. ThreadLocal是否会引发内存泄露的分析(转)

    这篇文章,主要解决一下疑惑: 1. ThreadLocal.ThreadLocalMap中提到的弱引用,弱引用究竟会不会被回收? 2. 弱引用什么情况下回收? 3. JAVA的ThreadLocal和 ...

  5. ThreadLocal的内存泄露(转)

    ThreadLocal的目的就是为每一个使用ThreadLocal的线程都提供一个值,让该值和使用它的线程绑定,当然每一个线程都可以独立地改变它绑定的值.如果需要隔离多个线程之间的共享冲突,可以使用T ...

  6. ThreadLocal可能引起的内存泄露(转)

    threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好 ...

  7. ThreadLocal是否会导致内存泄露

    什么是内存泄露? 维基百科的定义:[内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存],我的理解就是程序失去了对某段内存的控制,那么这段内存就算是泄露了. ThreadLocal为什么会导致 ...

  8. 并发编程(四)—— ThreadLocal源码分析及内存泄露预防

    今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ...

  9. ThreadLocal可能引起的内存泄露

    threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好 ...

随机推荐

  1. [原创]cocos2d-x研习录-第一阶 背景介绍 之 cocos2d家族史

    Cocos2D是一个2D开源游戏引擎,它最早是由Ricardo Quesada(阿根廷人,社区简称Riq)和他的朋友们用Python开发的,用于开发2D游戏和基于2D图形的任何应用.最早引擎的名字源自 ...

  2. How to Configure the Gradient Boosting Algorithm

    How to Configure the Gradient Boosting Algorithm by Jason Brownlee on September 12, 2016 in XGBoost ...

  3. Processor Speculative & pipeline

    https://en.wikipedia.org/wiki/Instruction-level_parallelism https://en.wikipedia.org/wiki/Instructio ...

  4. 正则表达式 ——python 基础

    一.引言 正则表达式是含有文本和特别字符的字符串,这些文本和特别字符描述的模式可以识别各种字符串. 正则表达式的强大之处在于特殊符号的应用,特殊符号定义了字符集合.子组匹配,模式重复次数...正是这些 ...

  5. .net 添加Cookie的4种方法

    第一种添加Cookie方法 HttpCookie myCookie = new HttpCookie("userrole"); myCookie.Values["a&qu ...

  6. 合并master分支到自己的分支

    切换到自己的分支(比如:self):git checkout self: 在自己分支下,推送自己的分支到github远端仓库:git push --set-upstream origin self: ...

  7. Andaroid L新特性

    1.Material Design”(材料设计)的全新设计理念,和Holo相比,Material Design更加色彩丰富,不像Holo那样灰暗 2.1)卡片风格(锁屏界面) 2)环动式设计  And ...

  8. LintCode "Submatrix Sum"

    Naive solution is O(n^4). But on 1 certain dimension, naive O(n^2) can be O(n) by this well-known eq ...

  9. 去掉IntelliJ IDEA的拼写检查

    Settings→Editor→Inspections→Spelling 去掉Spelling下的Typo复选框即可 来自为知笔记(Wiz)

  10. erlang使用leveldb

    用的是诺顿的开源库,参考url来自这里 下载 git clone git@github.com:/norton/lets.git 编译 cd lets ./rebar get-deps ./rebar ...