<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>

          內(nèi)存泄露的原因找到了,罪魁禍?zhǔn)拙尤皇荍ava TheadLocal

          共 3135字,需瀏覽 7分鐘

           ·

          2020-11-22 05:29



          ThreadLocal使用不規(guī)范,師傅兩行淚

          組內(nèi)來了一個實習(xí)生,看這小伙子春光滿面、精神抖擻、頭發(fā)微少,我心頭一喜:絕對是個潛力股。于是我找經(jīng)理申請親自來帶他,為了幫助小伙子快速成長,我給他分了一個需求,這不需求剛上線幾天就出網(wǎng)上問題了?后臺監(jiān)控服務(wù)發(fā)現(xiàn)內(nèi)存一直在緩慢上升,初步懷疑是內(nèi)存泄露。

          把實習(xí)生的PR都找出來仔細(xì)review,果然發(fā)現(xiàn)問題了。由于公司內(nèi)部代碼是保密的,這里簡單寫一個demo還原場景(忽略代碼風(fēng)格問題)。

          public?class?ThreadPoolDemo?{
          ????private?static?final?ThreadPoolExecutor?poolExecutor?=?new?ThreadPoolExecutor(5,?5,?1,?TimeUnit.MINUTES,?new?LinkedBlockingQueue<>());
          ????public?static?void?main(String[]?args)?throws?InterruptedException?{
          ????????for?(int?i?=?0;?i?100;?++i)?{
          ????????????poolExecutor.execute(new?Runnable()?{
          ????????????????@Override
          ????????????????public?void?run()?{
          ????????????????????ThreadLocal?threadLocal?=?new?ThreadLocal<>();
          ????????????????????threadLocal.set(new?BigObject());
          ????????????????????//?其他業(yè)務(wù)代碼
          ????????????????}
          ????????????});
          ????????????Thread.sleep(1000);
          ????????}
          ????}
          ????static?class?BigObject?{
          ????????//?100M
          ????????private?byte[]?bytes?=?new?byte[100?*?1024?*?1024];
          ????}
          }

          代碼分析:

          • 創(chuàng)建一個核心線程數(shù)和最大線程數(shù)都為10的線程池,保證線程池里一直會有10個線程在運行。
          • 使用for循環(huán)向線程池中提交了100個任務(wù)。
          • 定義了一個ThreadLocal類型的變量,Value類型是大對象。
          • 每個任務(wù)會向threadLocal變量里塞一個大對象,然后執(zhí)行其他業(yè)務(wù)邏輯。
          • 由于沒有調(diào)用線程池的shutdown方法,線程池里的線程還是會在運行。

          乍一看這代碼好像沒有什么問題,那為什么會導(dǎo)致服務(wù)GC后內(nèi)存還高居不下呢?

          代碼中給threadLocal賦值了一個大的對象,但是執(zhí)行完業(yè)務(wù)邏輯后沒有調(diào)用remove方法,最后導(dǎo)致線程池中10個線程的threadLocals變量中包含的大對象沒有被釋放掉,出現(xiàn)了內(nèi)存泄露。

          大家說說這樣的實習(xí)生還能留不?

          ThreadLocal的value值存在哪里?

          實習(xí)生說他以為線程任務(wù)結(jié)束了threadLocal賦值的對象會被JVM垃圾回收,很疑惑為什么會出現(xiàn)內(nèi)存泄露。作為師傅我肯定要給他把原理講透呀。

          ThreadLocal類提供set/get方法存儲和獲取value值,但實際上ThreadLocal類并不存儲value值,真正存儲是靠ThreadLocalMap這個類,ThreadLocalMap是ThreadLocal的一個靜態(tài)內(nèi)部類,它的key是ThreadLocal實例對象,value是任意Object對象。

          ThreadLocalMap類的定義

          static?class?ThreadLocalMap?{
          ????//?定義一個table數(shù)組,存儲多個threadLocal對象及其value值
          ????private?Entry[]?table;
          ????ThreadLocalMap(ThreadLocal?firstKey,?Object?firstValue)?{
          ????????table?=?new?Entry[INITIAL_CAPACITY];
          ????????int?i?=?firstKey.threadLocalHashCode?&?(INITIAL_CAPACITY?-?1);
          ????????table[i]?=?new?Entry(firstKey,?firstValue);
          ????????size?=?1;
          ????????setThreshold(INITIAL_CAPACITY);
          ????}
          ????//?定義一個Entry類,key是一個弱引用的ThreadLocal對象
          ????//?value是任意對象
          ????static?class?Entry?extends?WeakReference<ThreadLocal>?{
          ????????/**?The?value?associated?with?this?ThreadLocal.?*/
          ????????Object?value;
          ????????Entry(ThreadLocal?k,?Object?v)?{
          ????????????super(k);
          ????????????value?=?v;
          ????????}
          ????}
          ????//?省略其他
          }

          進一步分析ThreadLocal類的代碼,看set和get方法如何與ThreadLocalMap靜態(tài)內(nèi)部類關(guān)聯(lián)上。

          ThreadLocal類set方法

          public?class?ThreadLocal<T>?{
          ?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);
          ????}
          ????//?省略其他方法
          }

          set的邏輯比較簡單,就是獲取當(dāng)前線程的ThreadLocalMap,然后往map里添加KV,K是當(dāng)前ThreadLocal實例,V是我們傳入的value。這里需要注意一下,map的獲取是需要從Thread類對象里面取,看一下Thread類的定義。

          public?class?Thread?implements?Runnable?{
          ????ThreadLocal.ThreadLocalMap?threadLocals?=?null;
          ????//省略其他
          }

          Thread類維護了一個ThreadLocalMap的變量引用。

          ThreadLocal類get方法

          get獲取當(dāng)前線程的對應(yīng)的私有變量,是之前set或者通過initialValue的值,代碼如下:

          class?ThreadLocal<T>?{
          ????public?T?get()?{
          ????????Thread?t?=?Thread.currentThread();
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????if?(map?!=?null)?{
          ????????????ThreadLocalMap.Entry?e?=?map.getEntry(this);
          ????????????if?(e?!=?null)
          ????????????????return?(T)e.value;
          ????????}
          ????????return?setInitialValue();
          ????}
          }

          代碼邏輯分析:

          • 獲取當(dāng)前線程的ThreadLocalMap實例;
          • 如果不為空,以當(dāng)前ThreadLocal實例為key獲取value;
          • 如果ThreadLocalMap為空或者根據(jù)當(dāng)前ThreadLocal實例獲取的value為空,則執(zhí)行setInitialValue();

          ThreadLocal相關(guān)類的關(guān)系總結(jié)

          看了上面的分析是不是對Thread,ThreadLocal,ThreadLocalMap,Entry這幾個類之間的關(guān)系有點暈了,沒關(guān)系我專門畫了一個UML類圖來總結(jié)(忽略UML標(biāo)準(zhǔn)語法)。

          ThreadLocal相關(guān)類的關(guān)系
          • 每個線程是一個Thread實例,其內(nèi)部維護一個threadLocals的實例成員,其類型是ThreadLocal.ThreadLocalMap。
          • 通過實例化ThreadLocal實例,我們可以對當(dāng)前運行的線程設(shè)置一些線程私有的變量,通過調(diào)用ThreadLocal的set和get方法存取。
          • ThreadLocal本身并不是一個容器,我們存取的value實際上存儲在ThreadLocalMap中,ThreadLocal只是作為TheadLocalMap的key。
          • 每個線程實例都對應(yīng)一個TheadLocalMap實例,我們可以在同一個線程里實例化很多個ThreadLocal來存儲很多種類型的值,這些ThreadLocal實例分別作為key,對應(yīng)各自的value,最終存儲在Entry table數(shù)組中。
          • 當(dāng)調(diào)用ThreadLocal的set/get進行賦值/取值操作時,首先獲取當(dāng)前線程的ThreadLocalMap實例,然后就像操作一個普通的map一樣,進行put和get。

          ThreadLocal內(nèi)存模型原理

          經(jīng)過上面的分析我們對ThreadLocal相關(guān)的類設(shè)計已經(jīng)非常清楚了,下面通過一張圖更加深入理解一下ThreadLocal的內(nèi)存存儲。

          ThreadLocal內(nèi)存模型

          圖中左邊是棧,右邊是堆。線程的一些局部變量和引用使用的內(nèi)存屬于Stack(棧)區(qū),而普通的對象是存儲在Heap(堆)區(qū)。

          • 線程運行時,我們定義的TheadLocal對象被初始化,存儲在Heap,同時線程運行的棧區(qū)保存了指向該實例的引用,也就是圖中的ThreadLocalRef。
          • 當(dāng)ThreadLocal的set/get被調(diào)用時,虛擬機會根據(jù)當(dāng)前線程的引用也就是CurrentThreadRef找到其對應(yīng)在堆區(qū)的實例,然后查看其對用的TheadLocalMap實例是否被創(chuàng)建,如果沒有,則創(chuàng)建并初始化。
          • Map實例化之后,也就拿到了該ThreadLocalMap的句柄,那么就可以將當(dāng)前ThreadLocal對象作為key,進行存取操作。
          • 圖中的虛線,表示key對應(yīng)ThreadLocal實例的引用是個弱引用。

          強引用弱引用的概念

          ThreadLocalMap的key是一個弱引用類型,源代碼如下:

          static?class?ThreadLocalMap?{
          ????//?定義一個Entry類,key是一個弱引用的ThreadLocal對象
          ????//?value是任意對象
          ????static?class?Entry?extends?WeakReference<ThreadLocal>?{
          ????????/**?The?value?associated?with?this?ThreadLocal.?*/
          ????????Object?value;
          ????????Entry(ThreadLocal?k,?Object?v)?{
          ????????????super(k);
          ????????????value?=?v;
          ????????}
          ????}
          ????//?省略其他
          }

          下面解釋一下常見的幾種引用概念。

          強引用

          一直活著:類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象實例。

          弱引用

          回收就會死亡:被弱引用關(guān)聯(lián)的對象實例只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象實例。在JDK 1.2之后,提供了WeakReference類來實現(xiàn)弱引用。

          軟引用

          有一次活的機會:軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象實例列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類來實現(xiàn)軟引用。

          虛引用

          也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個對象實例是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象實例被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2之后,提供了PhantomReference類來實現(xiàn)虛引用。

          內(nèi)存泄露是不是弱引用的鍋?

          從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但是另一個問題也同樣值得思考:為什么ThreadLocalMap使用弱引用而不是強引用?

          翻看官網(wǎng)文檔的說法:

          To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.?

          為了處理非常大和長期的用途,哈希表條目使用weakreference作為鍵。

          分兩種情況討論:

          (1)key 使用強引用

          引用ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導(dǎo)致Entry內(nèi)存泄漏。

          (2)key 使用弱引

          引用ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調(diào)用set、get、remove的時候會被清除。

          比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應(yīng)key,都會導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal被清理后key為null,對應(yīng)的value在下一次ThreadLocalMap調(diào)用set、get、remove的時候可能會被清除。

          因此,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應(yīng)key就會導(dǎo)致內(nèi)存泄漏,而不是因為弱引用。

          ThreadLocal最佳實踐

          通過前面幾小節(jié)我們分析了ThreadLocal的類設(shè)計以及內(nèi)存模型,同時也重點分析了發(fā)生內(nèi)存泄露的條件和特定場景。最后結(jié)合項目中的經(jīng)驗給出建議使用ThreadLocal的場景:

          • 當(dāng)需要存儲線程私有變量的時候。
          • 當(dāng)需要實現(xiàn)線程安全的變量時。
          • 當(dāng)需要減少線程資源競爭的時候。

          綜合上面的分析,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果,那么怎么避免內(nèi)存泄漏呢?

          答案就是:每次使用完ThreadLocal,建議調(diào)用它的remove()方法,清除數(shù)據(jù)。

          另外需要強調(diào)的是并不是所有使用ThreadLocal的地方,都要在最后remove(),因為他們的生命周期可能是需要和項目的生存周期一樣長的,所以要進行恰當(dāng)?shù)倪x擇,以免出現(xiàn)業(yè)務(wù)邏輯錯誤!


          ? ? ? ?
          ???
          13 張圖徹底搞懂分布式系統(tǒng)服務(wù)注冊與發(fā)現(xiàn)原理
          分布式鏈路追蹤系統(tǒng)原來是這么一回事
          刨根問底,Kafka 消息中間件到底會不會丟消息

          覺得不錯,點個在看~

          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产jk在线 | 日本插逼视频 | 亚洲人在线 | 欧美 高潮喷高清 | 日韩婬乱片A片AAA真人视频 |