目錄
一.介紹
二.問題提出
2.1記憶體原理圖
2.2幾個問題
三.回答問題
3.1為什麼會出現記憶體洩漏
3.2若Entry使用弱引用
3.3弱引用配合自動回收
四.總結
一.介紹
之前使用ThreadLocal的時候,就聽過ThreadLocal怎麼怎麼的可能會出現記憶體洩漏,具體原因也沒去深究,就是一種不清不楚的狀態。最近在看JDK的原始碼,其中就包含ThreadLocal,在對ThreadLocal的使用場景介紹以及原始碼的分析後,對於ThreadLocal中可能存在的記憶體洩漏問題也搞清楚了,所以這裡專門寫一篇部落格分析一下。
在分析記憶體洩漏之前,先了解2個概念,就是記憶體洩漏和記憶體溢位:
記憶體溢位(memory overflow):是指不能申請到足夠的記憶體進行使用,就會發生記憶體溢位,比如出現的OOM(Out Of Memory)
記憶體洩漏(memory lack):記憶體洩露是指在程式中已經動態分配的堆記憶體由於某種原因未釋放或者無法釋放(已經沒有用處了,但是沒有釋放),造成系統記憶體的浪費,這種現象叫“記憶體洩露”。
當記憶體洩露到達一定規模後,造成系統能申請的記憶體較少,甚至無法申請記憶體,最終導致記憶體溢位,所以記憶體洩露是導致記憶體溢位的一個原因。
二.問題提出
2.1記憶體原理圖
下圖是程式執行中的記憶體分佈圖,簡要介紹一下這種圖:當前執行緒有一個threadLocals屬性(ThreadLocalMap屬性),該map的底層是陣列,每個陣列元素時Entry型別,Entry型別的key是ThreadLocal型別(也就是建立的ThreadLocal物件),而value是則是ThreadLocal.set()方法設定的value。
需要注意的是ThreadLocalMap的Entry,繼承自弱引用,定義如下,關於Java的引用介紹,可以參考:Java-強引用、軟引用、弱引用、虛引用
/** * ThreadLocalMap中存放的元素型別,繼承了弱引用類 */ static class Entry extends WeakReference<ThreadLocal<?>> { // key對應的value,注意key是ThreadLocal型別 Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
2.2問題提出
在看了上面ThreadLocal和ThreadLocalMap相關的記憶體分佈以及關聯後,提出這樣幾個問題:
1.ThreadLocal為什麼會出現記憶體溢位?
2.Entry的key為什麼要用弱引用?
3.使用弱引用是否就能解決記憶體溢位?
為了回答上面這3個問題,我寫了一段程式碼,後面根據這段程式碼進行分析:
public void step1() { // some action step2(); step3(); // other action } // 在stepX中都會建立threadLocal物件 public void step2() { ThreadLocal<String> tl = new ThreadLocal<>(); tl.set("this is value"); } public void step3() { ThreadLocal<Integer> tl = new ThreadLocal<>(); tl.set(99); }
在step1中會呼叫step2和step3,step2和step3都會建立ThreadLocal物件,當step2和step3執行完畢後,其中的棧記憶體中ThreadLocal引用就會被清除。
三.回答問題
現在針對這個圖,一步一步的分析問題,中途會得出一些臨時的結論,但是最終的結論才是正確的。
3.1為什麼會出現記憶體洩露
現在2點假設,本小節的分析都是基於這兩個假設之上的:
1.Entry使用強引用,key對ThreadLocal物件時強引用,也就是上面圖中連線5是強引用(key強引用ThreadLocal物件);
2.ThreadLocalMap中不會對過期的Entry進行清理。
上面程式碼中,如果ThreadLocalMap的key使用強引用,那麼即使棧記憶體的ThreadLocal引用被清除,但是堆中的ThreadLocal物件卻並不會被清除!!!
這是因為ThreadLocalMap中Entry的key對ThreadLocal物件是強引用,如果當前執行緒不結束,那麼ThreadLocal將會一直存在,對應的記憶體就不會被回收,與之關聯的Entry也不會被回收(Entry對應的value也不會被回收),當這種情況出現數量比較多的時候,未釋放的記憶體就會上升,就可能出現記憶體洩漏的問題。
上面的結論是暫時的,有前提假設!!!最終結論還需要看後面分析。
3.2若Entry使用弱引用
仍舊有1個假設,就是ThreadLocalMap中不會對過期的Entry進行清理,陳舊的Entry是指Entry的key為null。
按照原始碼,Entry繼承弱引用,其Key對ThreadLocal是弱引用,也就是上圖中連線5是弱引用,連線6仍為強引用。
同樣以上面程式碼為例,step2和step3建立了ThreadLocal物件,step2和step3執行完後,棧中的ThreadLocal引用被清除了;由於堆記憶體中ThreadLocalMap的Entry key弱引用ThreadLocal物件,根據垃圾收集器對弱引用物件的處理:
當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。
此時堆中ThreadLocal物件會被gc回收,Entry的key為null,但是value不為null,且value也是強引用(連線6),所以Entry仍舊不能回收,只能釋放ThreadLocal的記憶體,仍舊可能導致記憶體洩漏。
在沒有自動清理陳舊Entry的前提下,即使Entry使用弱引用,仍可能出現記憶體洩漏。
3.3弱引用配合自動回收
通過3.3的分析,其實只要陳舊的Entry能自動被回收,就能解決記憶體洩漏的問題,當然JDK就是這麼做的。
如果看過原始碼,就知道,ThreadLocalMap底層使用陣列來儲存元素,使用“線性探測法”來解決hash衝突,關於線性探測法的介紹可以檢視:利用線性探測法解決hash衝突
在每次呼叫ThreadLocal類的get、set、remove這些方法的時候,內部其實都是對ThreadLocalMap進行操作,對應ThreadLocalMap的get、set、remove操作。
重點來了!重點來了!重點來了!
ThreadLocalMap的每次get、set、remove,都會清理過期的Entry,下面以get操作解釋,其他操作也是一個意思,大致如下:
1.ThreadLocalMap底層用陣列儲存元素,當get一個Entry時,根據key的hash值(非hashCode)計算出該Entry應該出在什麼位置;
2.計算出的位置可能會有衝突,比如預期位置是position=5,但是position=5的位置已經有其他Entry了;
3.出現衝突後,會使用線性探測法,找position=6位置上的Entry是否匹配(匹配是指hash相同),如果匹配,則返回position=6的Entry。
4.在這個過程中,如果position=5位置上的Entry已經是陳舊的Entry(Entry的key為null),此時position=5的key就應該被清理;
5.光清理position=5的Entry還不夠,為了保證線性探測法的規則,需要判斷陣列中的其他元素是否需要調整位置(如果需要,則調整位置),在這個過程中,也會進行清理陳舊Entry的操作。
上面這5個步驟就保證了每次get都會清理陣列中(map)的陳舊Entry,清理一個陳舊的Entry,就是下面這三行程式碼:
Entry.value = null; // 將Entry的value設為null table[index] = null;// 將陣列中該Entry的位置設定null size--; // map的size減一
對於ThreadLocal的set、remove也類似這個原理。
有了自動回收陳舊Entry的操作,需要注意的是,在這個時候,key使用弱引用就是至關重要的一點!!!
因為key使用弱引用後,當弱引用的ThreadLocal物件被會回收後,該key的引用為null,則該Entry在下一次get、set、remove的時候就才會被清理,從未避免記憶體洩漏的問題。
四.總結
在上面的分析中,看到ThreadLocal基本不會出現記憶體洩漏的問題了,因為ThreadLocalMap中會在get、set、remove的時候清理陳舊的Entry,與Entry的key使用弱引用密不可分。
當然我們也可以在程式碼中手動呼叫ThreadLocal的remove方法進行清除map中中key為該threadLocal物件的Entry,同時清理過期的Entry。