溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

內(nèi)存泄露的原因是什么

發(fā)布時(shí)間:2021-10-25 17:28:35 來源:億速云 閱讀:149 作者:iii 欄目:編程語言

這篇文章主要介紹“內(nèi)存泄露的原因是什么”,在日常操作中,相信很多人在內(nèi)存泄露的原因是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”內(nèi)存泄露的原因是什么”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

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

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

把實(shí)習(xí)生的PR都找出來仔細(xì)review,果然發(fā)現(xiàn)問題了。由于公司內(nèi)部代碼是保密的,這里簡單寫一個(gè)demo還原場(chǎng)景(忽略代碼風(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<BigObject> 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)建一個(gè)核心線程數(shù)和最大線程數(shù)都為10的線程池,保證線程池里一直會(huì)有10個(gè)線程在運(yùn)行。

  • 使用for循環(huán)向線程池中提交了100個(gè)任務(wù)。

  • 定義了一個(gè)ThreadLocal類型的變量,Value類型是大對(duì)象。

  • 每個(gè)任務(wù)會(huì)向threadLocal變量里塞一個(gè)大對(duì)象,然后執(zhí)行其他業(yè)務(wù)邏輯。

  • 由于沒有調(diào)用線程池的shutdown方法,線程池里的線程還是會(huì)在運(yùn)行。

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

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

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

ThreadLocal的value值存在哪里?

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

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

ThreadLocalMap類的定義

static class ThreadLocalMap {     // 定義一個(gè)table數(shù)組,存儲(chǔ)多個(gè)threadLocal對(duì)象及其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);     }     // 定義一個(gè)Entry類,key是一個(gè)弱引用的ThreadLocal對(duì)象     // value是任意對(duì)象     static class Entry extends WeakReference<ThreadLocal<?>> {         /** The value associated with this ThreadLocal. */         Object value;         Entry(ThreadLocal<?> k, Object v) {             super(k);             value = v;         }     }     // 省略其他 }

進(jìn)一步分析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實(shí)例,V是我們傳入的value。這里需要注意一下,map的獲取是需要從Thread類對(duì)象里面取,看一下Thread類的定義。

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

Thread類維護(hù)了一個(gè)ThreadLocalMap的變量引用。

ThreadLocal類get方法

get獲取當(dāng)前線程的對(duì)應(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實(shí)例;

  • 如果不為空,以當(dāng)前ThreadLocal實(shí)例為key獲取value;

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

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

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

內(nèi)存泄露的原因是什么


ThreadLocal相關(guān)類的關(guān)系

  • 每個(gè)線程是一個(gè)Thread實(shí)例,其內(nèi)部維護(hù)一個(gè)threadLocals的實(shí)例成員,其類型是ThreadLocal.ThreadLocalMap。

  • 通過實(shí)例化ThreadLocal實(shí)例,我們可以對(duì)當(dāng)前運(yùn)行的線程設(shè)置一些線程私有的變量,通過調(diào)用ThreadLocal的set和get方法存取。

  • ThreadLocal本身并不是一個(gè)容器,我們存取的value實(shí)際上存儲(chǔ)在ThreadLocalMap中,ThreadLocal只是作為TheadLocalMap的key。

  • 每個(gè)線程實(shí)例都對(duì)應(yīng)一個(gè)TheadLocalMap實(shí)例,我們可以在同一個(gè)線程里實(shí)例化很多個(gè)ThreadLocal來存儲(chǔ)很多種類型的值,這些ThreadLocal實(shí)例分別作為key,對(duì)應(yīng)各自的value,最終存儲(chǔ)在Entry  table數(shù)組中。

  • 當(dāng)調(diào)用ThreadLocal的set/get進(jìn)行賦值/取值操作時(shí),首先獲取當(dāng)前線程的ThreadLocalMap實(shí)例,然后就像操作一個(gè)普通的map一樣,進(jìn)行put和get。

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

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

內(nèi)存泄露的原因是什么


ThreadLocal內(nèi)存模型

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

  • 線程運(yùn)行時(shí),我們定義的TheadLocal對(duì)象被初始化,存儲(chǔ)在Heap,同時(shí)線程運(yùn)行的棧區(qū)保存了指向該實(shí)例的引用,也就是圖中的ThreadLocalRef。

  • 當(dāng)ThreadLocal的set/get被調(diào)用時(shí),虛擬機(jī)會(huì)根據(jù)當(dāng)前線程的引用也就是CurrentThreadRef找到其對(duì)應(yīng)在堆區(qū)的實(shí)例,然后查看其對(duì)用的TheadLocalMap實(shí)例是否被創(chuàng)建,如果沒有,則創(chuàng)建并初始化。

  • Map實(shí)例化之后,也就拿到了該ThreadLocalMap的句柄,那么就可以將當(dāng)前ThreadLocal對(duì)象作為key,進(jìn)行存取操作。

  • 圖中的虛線,表示key對(duì)應(yīng)ThreadLocal實(shí)例的引用是個(gè)弱引用。

強(qiáng)引用弱引用的概念

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

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

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

強(qiáng)引用

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

弱引用

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

軟引用

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

虛引用

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

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

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

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

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

為了處理非常大和長期的用途,哈希表?xiàng)l目使用weakreference作為鍵。

分兩種情況討論:

(1)key 使用強(qiáng)引用

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

(2)key 使用弱引

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

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

因此,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動(dòng)刪除對(duì)應(yīng)key就會(huì)導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?/p>

ThreadLocal最佳實(shí)踐

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

  • 當(dāng)需要存儲(chǔ)線程私有變量的時(shí)候。

  • 當(dāng)需要實(shí)現(xiàn)線程安全的變量時(shí)。

  • 當(dāng)需要減少線程資源競爭的時(shí)候。

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

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

另外需要強(qiáng)調(diào)的是并不是所有使用ThreadLocal的地方,都要在最后remove(),因?yàn)樗麄兊纳芷诳赡苁切枰晚?xiàng)目的生存周期一樣長的,所以要進(jìn)行恰當(dāng)?shù)倪x擇,以免出現(xiàn)業(yè)務(wù)邏輯錯(cuò)誤!

到此,關(guān)于“內(nèi)存泄露的原因是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI