Java内存泄露学习 ThreadLocal真的会内存泄露吗

概述

ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。比如我们熟知的Spring事务管理中就使用了ThreadLocal来保证多线程环境下connection的线程安全问题。再比如我们日常的java web项目开发中,经常使用ThreadLocal来存储一些用户id等信息,在一次request请求中,首先拿到登录的uid,然后放到ThreadLocal上下文中,这样在service、dao层就不需要一直传递uid直接从上下文中获取就可以了。

但是说起ThreadLocal,除了好用我们还经常听到的就是它有内存泄露的风险,那么到底是怎么产生内存泄露的呢?难道jdk设计的类还会内存泄露吗?我们应该怎么样避免内存泄露呢?

ThreadLocal内部设计

关于ThreadLocal的使用说明就不提了,直接引用网上常见的一个图来描述一下对象的引用关系


image.png

简单解释一下这个图,假设有这样几行代码

Thread t = new Thread(()->{
    ThreadLocal tl = new ThreadLocal();
    tl.set(object);
});
t.start();

Thread类定义有一个property叫做ThreadLocal.ThreadLocalMap threadLocals
threadLocals内部是一个hashmap类似的结构,存储着很多Entry
上面的代码操作后的结果就是
Entry的key是 tl, value是object

那么栈上面的引用t代表的就是CurrentThreadRef,指向new Thread这个对象。tl代表的就是ThreadLocalRef,指向的就是new ThreadLocal这个对象。

内存泄露探究

关于ThreadLocal的内存泄露讨论可能是由于ThreadLocal在我们平时代码使用中越来越频繁,又或许是高频面试题的原因被讨论的越来越多。现在网上关于ThreadLocal内存泄露的分析文章非常之多,但是我觉得并不全面,或者仅仅是提了一下弱引用这个问题就完了。
内存泄露通常说的是key被回收后,value无法被访问到但是仍然占用了内存,key的弱引用当然是最核心的点,但是是否内存泄露还跟我们的使用场景有关。通过上面的图解分析我们可以发现:
1、如果Thread生命周期比较长,是线程池场景,比如tomcat worker线程。那么除非ThreadLocal ref强引用被释放掉,gc就会回收ThreadLocal对象,导致ThreadLocalMap中之前该ThreadLocal对应的value无法回收,内存泄露。
2、上一种情况下,ThreadLocal ref强引用什么情况下会释放呢,如果我们平时使用的时候都是将ThreadLocal定义为static的变量,那么强引用是不会被释放的,所以这时候key的弱引用就没有那么重要了。
3、如果Thread本身生命周期结束了,CurrentThread ref强引用释放了,gc以后ThreadLocalMap就完全被回收了,不会产生内存泄露。

场景一
@Controller
@RequestMapping("/myThreadLocal")
public class MyThreadLocalTestController {

    private static ThreadLocal staticThreadLocal = new MyThreadLocal();

    /**
     * ThreadLocal变量为非静态变量,使用完以后释放掉强引用,
     * 只剩下threadLocalMap中entry的key这个弱引用,gc可以回收掉ThreadLocal对象
     * 但是value My50MB对象不会被回收,除非thread的生命周期结束
     * @return
     */
    @RequestMapping(value = "/nonStaticWithTomcatThread", method = RequestMethod.GET)
    @ResponseBody
    public String nonStaticWithTomcatThread() {
        System.out.println("staticThreadLocal.hashCode=" + staticThreadLocal.hashCode());
        ThreadLocal t = new MyThreadLocal();
        System.out.println("t.hashCode=" + t.hashCode());
        t.set(new My50MB());// 注意禁用jvm逃逸分析的优化 -XX:-DoEscapeAnalysis

        t = null; // 释放掉强引用
        try {
            System.gc();// 提示系统gc
            TimeUnit.SECONDS.sleep(5L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "ok";
    }
}

这种情况下Thread是池化的,但是ThreadLocal ref强引用会被释放。
启动tomcat运行这段代码,我们分3次获取内存dump信息
第一次在tomcat刚启动成功,得到0.hprof
第二次在首次访问/nonStaticWithTomcatThread这个请求后,得到1.hprof
第三次在二次访问/nonStaticWithTomcatThread这个请求后,得到2.hprof

用MAT工具打开3个dump文件,查看Histogram信息,发现byte[]这个内存的占用一次比一次多,每次多出50MB。打开with incoming reference 分别看到如下信息

0.png

0.hprof中看到最大的一个内存占用是静态变量staticThreadLocal中有一个1MB的byte[],这个跟我们的示例没有关系

1.png

1.hprof中看到最大的一个内存占用是My50MB对象内部的一个byte[],My50MB被ThreadLocalMap$Entry引用,所以很明显这个50MB就是那个内存泄露的value。

2.png

2.hprof中看到有2个My50MB对象都引用了一个50MB的byte[],跟1.hprof一模一样只是多了一个,因为我们访问了2次controller。如果我们访问的次数越多,这个内存泄露就越来越明显。

场景二
    @RequestMapping(value = "/staticWithTomcatThread", method = RequestMethod.GET)
    @ResponseBody
    public String staticWithTomcatThread() {
        staticThreadLocal.set(new My50MB());
        /**try {
            // invoke service to do business
        } finally {
            staticThreadLocal.remove();
        }**/
        return "ok";
    }

这种情况下Thread仍然是池化的,ThreadLocal ref强引用是不会被释放的,如果还是调用2次controller方法,打印出来的dump文件是始终只会有一个My50MB存在,前提是2次是同一个线程对象(如果tomcat线程有n个,n个请求同时访问,每一个线程都会存在一个My50MB的对象,不考虑内存溢出的情况下),这里就不拿heap dump分析了。

当然针对这种池化的线程,ThreadLocal就相当于给这个线程增加状态信息,线程复用的情况下容易出现业务逻辑错误。所以我们一般在使用线程处理完业务逻辑后要清理掉线程中的状态信息,也就是加上代码中被注释掉的那段代码。 这样不但能避免逻辑错误,也可以使线程在非活跃状态下系统内存占用的更少,如果不调用remove方法清理其实也是一定程度的内存泄露。

场景三
    @RequestMapping(value = "/staticWithNewThread", method = RequestMethod.GET)
    @ResponseBody
    public String staticWithNewThread() {
        new Thread(()->{
            staticThreadLocal.set(new My50MB());
        }).start();
        return "ok";
    }

这种情况下Thread生命周期在代码执行完毕后就会结束,Thread内部的threadLocalMap就会被内存回收了,所以不存在任何泄露的问题。看起来set了一个对象到staticThreadLocal中,但是其实ThreadLocal只是一个工具,真实存储是在Thread中,不能被表象所迷惑。

ThreadLocal对内存泄露的预防

其实jdk将ThreadLocalMap$Entry的key设计为WeakReference的时候就已经考虑了value的内存泄露问题,我们看看ThreadLocalMap的注释

/**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

最后一句话的意思大概就是当map的空间占用过大后,那么弱引用的key被回收后,无用的entries就会被清理掉。在get() set()操作的时候都会有一些时机触发,具体可以自行看源码


image.png

总结

1、ThreadLocal只是一个工具,具体的变量存储是放在Thread中的,所以内存泄露很大程度上要看Thread的生命周期
2、ThreadLocalMap$Entry中的key是弱引用,要防止key对象被回收造成value对象的内存泄露
3、ThreadLocal一般都应该定义成static变量
4、如果在线程池场景下使用ThreadLocal一定要记得调用remove

相关阅读:

当ThreadLocal碰上线程池 https://www.jianshu.com/p/85d96fe9358b

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容