ThreadLocal 内存泄露问题

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 ——百度百科

上述的意思用在 java 中就是存在已经没有任何引用的对象,但是 GC 又不能把对象所在的内存回收掉,所以就造成了内存泄漏。

我们知道ThreadLocal 主要解决的是对象不能被多个线程同时访问的问题。根据 ThreadLocal 的源码看看它是怎么实现的。

ThreadLocal 设置数据的set()方法

public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
  }

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看到在使用 ThreadLocal 设置数据时,其实设置到的是当前线程的 threadLocals 字段里,去 Thread 里看一看 threadLocals 变量

ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals 的类型是 ThreadLocal 里的内部类 ThreadLocalMap,ThreadLocalMap 的中用来存储数据的又是一个内部类是Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
      Object value;

      Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
}

Entry的 key 是当前 ThreadLocal,value 值是我们要设置的数据。

WeakReference表示的是弱引用,当 JVM 进行 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。

因为 WeakReference<ThreadLocal<?>>,所以在EntryThreadLocal是弱引用,一旦发生 GC,ThreadLocal会被 GC 回收掉,但是value是强引用,它不会被回收掉。用一张图来表示一下

image.png

图中实线表示的是强引用,虚线表示的是弱引用。

当JVM发生GC后,虚线会断开应用,也就是key会变为null,value是强引用不会为null,整个Entry也不为null,它依然在ThreadLocalMap中,并占据着内存,

我们获取数据时,使用ThreadLocal的get()方法,ThreadLocal并不为null,所以我们无法通过一个key为null去访问到该entry的value。这就造成了内存泄漏。

既然用弱引用会造成内存泄漏,直接用强引用可以么?

答案是不行。如果是强引用的话,看看下面代码

 ThreadLocal threadLocal = new ThreadLocal();
 threadLocal.set(new Object());
 threadLocal = null;

我们在设置完数据后,直接将threadLocal设为null,这时栈中ThreadLocal Ref 到堆中ThreadLocal断开了,但是keyThreadLocal的引用依然存在,GC依旧没法回收,同样会造成内存泄漏。

那弱引用比强引用好在哪?

当key为弱引用时,同样是上面代码,当threadLocal设为null时,栈中ThreadLocal Ref 到堆中ThreadLoacl断开了,keyThreadLoacl也因为GC断开了,这时ThreadLocal就可以被回收了。

同时,ThreadLocal也可以根据key.get() == null 来判断key是否已经被回收,因此ThreadLocal可以自己清理这些过期的节点来避免内存泄漏。

其实,ThreadLocal做了很大的工作清除过期的key来避免发生内存泄漏

  1. 在调用set()方法时,会进行清理

     private void set(ThreadLocal<?> key, Object value) {
    
         Entry[] tab = table;
         int len = tab.length;
         int i = key.threadLocalHashCode & (len-1);
    
         for (Entry e = tab[i];
              e != null;
              e = tab[i = nextIndex(i, len)]) {
              ThreadLocal<?> k = e.get();
    
              if (k == key) {
                  e.value = value;
                  return;
               }
             // 当key为null时,替换掉
               if (k == null) {
                   replaceStaleEntry(key, value, i);
                   return;
               }
         }
    
         tab[i] = new Entry(key, value);
         int sz = ++size;
         // 清理一些槽位,清理过期key
         if (!cleanSomeSlots(i, sz) && sz >= threshold)
             rehash();
     }
    
    

    1、 当key为null时,说明该位置被GC回收了,会将当前位置覆盖掉。

    2、 在set()方法最后调用了cleanSomeSlots()中还会有清理的操作。看一看cleanSomeSlots()

     private boolean cleanSomeSlots(int i, int n) {
         boolean removed = false;
         Entry[] tab = table;
         int len = tab.length;
         do {
             i = nextIndex(i, len);
             Entry e = tab[i];
             if (e != null && e.get() == null) {
                 n = len;
                 removed = true;
                 // 真正的清理工作
                 i = expungeStaleEntry(i);
              }
          } while ( (n >>>= 1) != 0);
          return removed;
     }
    
    

    cleanSomeSlots()中当判断e != null && e.get() == null为true时,说明已经被GC回收了,会调用expungeStaleEntry()进行清理工作,具体的逻辑就不再看了。

  1. 在调用get()方法时,如果没有命中,会向后查找,也会进行清理操作

     
    private Entry getEntry(ThreadLocal<?> key) {
         int i = key.threadLocalHashCode & (table.length - 1);
         Entry e = table[i];
         if (e != null && e.get() == key)
             return e;
         else
             // 没有命中向后查找
            return getEntryAfterMiss(key, i, e);
     }
     private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
         Entry[] tab = table;
         int len = tab.length;
    
         while (e != null) {
             ThreadLocal<?> k = e.get();
             if (k == key)
                 return e;
             if (k == null)
                 // 当key为null,说明被GC回收了,进行清理的操作
                 expungeStaleEntry(i);
             else
                 i = nextIndex(i, len);
             e = tab[i];
         }
         return null;
     }
    
  1. 调用remove()时,除了清理当前节点,还会向后进行清理操作

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

推荐阅读更多精彩内容