1. 驚!ThreadLocal你怎么動(dòng)不動(dòng)就內(nèi)存泄漏?

        共 10831字,需瀏覽 22分鐘

         ·

        2021-05-24 16:17

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

        前言

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

        正文

        我們首先看一個(gè)例子,代碼如下:

        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)建了一個(gè)核心線程數(shù)和最大線程數(shù)為 6 的線程池,這個(gè)保證了線程池里面隨時(shí)都有 6 個(gè)線程在運(yùn)行。

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

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

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

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

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

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

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

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

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

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

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

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

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

        接著我們?cè)俳又?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(71, 193, 168);">Entry的父類WeakReference的構(gòu)造函數(shù)super(k),如下所示:

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

        接著我們?cè)倏?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(71, 193, 168);">WeakReference的父類Reference的構(gòu)造函數(shù)super(referent),如下所示:

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

        接著我們?cè)倏?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(71, 193, 168);">WeakReference的父類Reference的另外一個(gè)構(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 對(duì)象的弱引用,具體是 referent 變量引用了 ThreadLocal 對(duì)象,value 為具體調(diào)用 ThreadLocalset 方法傳遞的值。

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

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

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

        private void remove(ThreadLocal<?> key) {

          //(1)計(jì)算當(dāng)前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方法清除對(duì)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,則去掉對(duì)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 方法,實(shí)際調(diào)用的是父類 WeakReferenceclear 方法,作用是去掉對(duì) ThreadLocal 的弱引用。

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

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

        總結(jié)

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

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

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

        — 【 THE END 】—
        本公眾號(hào)全部博文已整理成一個(gè)目錄,請(qǐng)?jiān)诠娞?hào)里回復(fù)「m」獲??!

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

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

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

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

        瀏覽 47
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 国产精品久久成人 | 久久亚洲电影 | 成人网站 视频免费上海一 | 国产靠比| 国产操逼视频在线 |