<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          驚!ThreadLocal你怎么動不動就內(nèi)存泄漏?

          共 10831字,需瀏覽 22分鐘

           ·

          2021-05-24 16:17

          今天無聊帶大家分析下ThreadLocal為什么會內(nèi)存泄漏~

          前言

          使用 ThreadLocal 不當可能會導致內(nèi)存泄露,是什么原因?qū)е碌?strong style="color: rgb(71, 193, 168);">內(nèi)存泄漏呢?

          正文

          我們首先看一個例子,代碼如下:

          public class ThreadLocalOutOfMemoryTest {
              static class LocalVariable {
                  private Long[] a = new Long[1024*1024];
              }

              // (1)
              final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(661, TimeUnit.MINUTES,
                      new LinkedBlockingQueue<>());
              // (2)
              final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

              public static void main(String[] args) throws InterruptedException {
                  // (3)
                  for (int i = 0; i < 50; ++i) {
                      poolExecutor.execute(new Runnable() {
                          public void run() {
                              // (4)
                              localVariable.set(new LocalVariable());
                              // (5)
                              System.out.println("use local varaible");
          //                    localVariable.remove();

                          }
                      });

                      Thread.sleep(1000);
                  }
                  // (6)
                  System.out.println("pool execute over");
              }
          }

          代碼(1)創(chuàng)建了一個核心線程數(shù)和最大線程數(shù)為 6 的線程池,這個保證了線程池里面隨時都有 6 個線程在運行。

          代碼(2)創(chuàng)建了一個 ThreadLocal 的變量,泛型參數(shù)為 LocalVariable,LocalVariable 內(nèi)部是一個 Long 數(shù)組。

          代碼(3)向線程池里面放入 50 個任務(wù)。

          代碼(4)設(shè)置當前線程的 localVariable 變量,也就是把 new 的 LocalVariable 變量放入當前線程的 threadLocals 變量。

          由于沒有調(diào)用線程池的 shutdown 或者 shutdownNow 方法所以線程池里面的用戶線程不會退出,進而 JVM 進程也不會退出。

          運行后,我們立即打開jconsole 監(jiān)控堆內(nèi)存變化,如下圖:

          接著,讓我們打開 localVariable.remove() 注釋,然后在運行,觀察堆內(nèi)存變化如下:

          從第一次運行結(jié)果可知,當主線程處于休眠時候進程占用了大概 75M 內(nèi)存,打開 localVariable.remove() 注釋后第二次運行則占用了大概 25M 內(nèi)存,可知 沒有寫 localVariable.remove() 時候內(nèi)存發(fā)生了泄露,下面分析下泄露的原因,如下:

          第一次運行的代碼,在設(shè)置線程的 localVariable 變量后沒有調(diào)用localVariable.remove() 方法,導致線程池里面的 5 個線程的 threadLocals 變量里面的new LocalVariable()實例沒有被釋放,雖然線程池里面的任務(wù)執(zhí)行完畢了,但是線程池里面的 5 個線程會一直存在直到 JVM 退出。這里需要注意的是由于 localVariable 被聲明了 static,雖然線程的 ThreadLocalMap 里面是對 localVariable 的弱引用,localVariable 也不會被回收。運行結(jié)果二的代碼由于線程在設(shè)置 localVariable 變量后即使調(diào)用了localVariable.remove()方法進行了清理,所以不會存在內(nèi)存泄露。

          接下來我們要想清楚的知道內(nèi)存泄漏的根本原因,那么我們就要進入源碼去看了。

          我們知道ThreadLocal 只是一個工具類,具體存放變量的是在線程的 threadLocals 變量里面,threadLocals 是一個 ThreadLocalMap 類型的,我們首先一覽ThreadLocalMap的類圖結(jié)構(gòu),類圖結(jié)構(gòu)如下圖:

          如上圖 ThreadLocalMap 內(nèi)部是一個 Entry 數(shù)組, Entry 繼承自 WeakReference,Entry 內(nèi)部的 value 用來存放通過 ThreadLocalset 方法傳遞的值,那么 ThreadLocal 對象本身存放到哪里了嗎?

          下面看看 Entry 的構(gòu)造函數(shù),如下所示:

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

          接著我們再接著看Entry的父類WeakReference的構(gòu)造函數(shù)super(k),如下所示:

          public WeakReference(T referent) {
             super(referent);
          }

          接著我們再看WeakReference的父類Reference的構(gòu)造函數(shù)super(referent),如下所示:

          Reference(T referent) {
             this(referent, null);
          }

          接著我們再看WeakReference的父類Reference的另外一個構(gòu)造函數(shù)this(referent , null),如下所示:

          Reference(T referent, ReferenceQueue<? super T> queue) {
             this.referent = referent;
             this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
          }

          可知 k 被傳遞到了 WeakReference 的構(gòu)造函數(shù)里面,也就是說 ThreadLocalMap 里面的 keyThreadLocal 對象的弱引用,具體是 referent 變量引用了 ThreadLocal 對象,value 為具體調(diào)用 ThreadLocalset 方法傳遞的值。

          當一個線程調(diào)用 ThreadLocal 的 set 方法設(shè)置變量時候,當前線程的 ThreadLocalMap 里面就會存放一個記錄,這個記錄的 keyThreadLocal 的引用,value 則為設(shè)置的值。

          但是考慮如果這個 ThreadLocal 變量沒有了其他強依賴,而當前線程還存在的情況下,由于線程的 ThreadLocalMap 里面的 key 是弱依賴,則當前線程的 ThreadLocalMap 里面的 ThreadLocal 變量的弱引用會被在 gc 的時候回收,但是對應(yīng) value 還是會造成內(nèi)存泄露,這時候 ThreadLocalMap 里面就會存在 keynull 但是 value 不為 nullentry 項。

          其實在 ThreadLocalsetgetremove 方法里面有一些時機是會對這些 keynullentry 進行清理的,但是這些清理不是必須發(fā)生的,下面簡單講解ThreadLocalMapremove 方法的清理過程,remove 的源碼,如下所示:

          private void remove(ThreadLocal<?> key) {

            //(1)計算當前ThreadLocal變量所在table數(shù)組位置,嘗試使用快速定位方法
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            //(2)這里使用循環(huán)是防止快速定位失效后,變量table數(shù)組
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                //(3)找到
                if (e.get() == key) {
                    //(4)找到則調(diào)用WeakReference的clear方法清除對ThreadLocal的弱引用
                    e.clear();
                    //(5)清理key為null的元素
                    expungeStaleEntry(i);
                    return;
                }
             }
          }
           private int expungeStaleEntry(int staleSlot) {
                      Entry[] tab = table;
                      int len = tab.length;
                      //(6)去掉去value的引用
                      tab[staleSlot].value = null;
                      tab[staleSlot] = null;
                      size--;
                      Entry e;
                      int i;
                      for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
                          ThreadLocal<?> k = e.get();
                          //(7)如果key為null,則去掉對value的引用。
                          if (k == null) {
                              e.value = null;
                              tab[i] = null;
                              size--;
                          } else {
                              int h = k.threadLocalHashCode & (len - 1);
                              if (h != i) {
                                  tab[i] = null;
                                  while (tab[h] != null)
                                      h = nextIndex(h, len);
                                  tab[h] = e;
                              }
                          }
                      }
                      return i;
            }

          代碼(4)調(diào)用了 Entryclear 方法,實際調(diào)用的是父類 WeakReferenceclear 方法,作用是去掉對 ThreadLocal 的弱引用。

          代碼(6)是去掉對 value 的引用,到這里當前線程里面的當前 ThreadLocal 對象的信息被清理完畢了。

          代碼(7)從當前元素的下標開始看 table 數(shù)組里面的其他元素是否有 keynull 的,有則清理。循環(huán)退出的條件是遇到 table 里面有 null 的元素。所以這里知道 null 元素后面的 Entry 里面 keynull 的元素不會被清理。

          總結(jié)

          1. ThreadLocalMap 內(nèi)部 Entrykey 使用的是對 ThreadLocal 對象的弱引用,這為避免內(nèi)存泄露是一個進步,因為如果是強引用,那么即使其他地方?jīng)]有對 ThreadLocal 對象的引用,ThreadLocalMap 中的 ThreadLocal 對象還是不會被回收,而如果是弱引用則這時候 ThreadLocal 引用是會被回收掉的。

          2. 但是對于的 value 還是不能被回收,這時候 ThreadLocalMap 里面就會存在 keynull 但是 value 不為 nullentry 項,雖然 ThreadLocalMap 提供了 set,get,remove 方法在一些時機下會對這些 Entry 項進行清理,但是這是不及時的,也不是每次都會執(zhí)行的,所以一些情況下還是會發(fā)生內(nèi)存泄露,所以在使用完畢后即使調(diào)用 remove 方法才是解決內(nèi)存泄露的最好辦法。

          3. 線程池里面設(shè)置了 ThreadLocal 變量一定要記得及時清理,因為線程池里面的核心線程是一直存在的,如果不清理,那么線程池的核心線程的 threadLocals 變量一直會持有 ThreadLocal 變量。

          — 【 THE END 】—
          本公眾號全部博文已整理成一個目錄,請在公眾號里回復「m」獲?。?/span>

          最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

          獲取方式:點“在看”,關(guān)注公眾號并回復 PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

          文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

          謝謝支持喲 (*^__^*)

          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伊人色色色 | 姝姝窝人体色www国产 | 亚洲精品久久久日产欧美蜜桃 | 麻豆视频免费观看 | 色就是色setufree |