為何每次用完 ThreadLocal 都要調(diào)用 remove()
- 什么是內(nèi)存泄漏
- Key 的泄漏
- Value 的泄漏
- 如何避免內(nèi)存泄露
什么是內(nèi)存泄漏
內(nèi)存泄漏指的是,當某一個對象不再有用的時候,占用的內(nèi)存卻不能被回收,這就叫作內(nèi)存泄漏。
因為通常情況下,如果一個對象不再有用,那么我們的垃圾回收器 GC,就應該把這部分內(nèi)存給清理掉。這樣的話,就可以讓這部分內(nèi)存后續(xù)重新分配到其他的地方去使用;否則,如果對象沒有用,但一直不能被回收,這樣的垃圾對象如果積累的越來越多,則會導致我們可用的內(nèi)存越來越少,最后發(fā)生內(nèi)存不夠用的 OOM 錯誤。
下面我們來分析一下,在 ThreadLocal 中這樣的內(nèi)存泄漏是如何發(fā)生的。
Key 的泄漏
在上一講中,我們分析了 ThreadLocal 的內(nèi)部結(jié)構(gòu),知道了每一個 Thread 都有一個 ThreadLocal.ThreadLocalMap 這樣的類型變量,該變量的名字叫作 threadLocals。線程在訪問了 ThreadLocal 之后,都會在它的 ThreadLocalMap 里面的 Entry 中去維護該 ThreadLocal 變量與具體實例的映射。
我們可能會在業(yè)務代碼中執(zhí)行了 ThreadLocal instance = null 操作,想清理掉這個 ThreadLocal 實例,但是假設我們在 ThreadLocalMap 的 Entry 中強引用了 ThreadLocal 實例,那么,雖然在業(yè)務代碼中把 ThreadLocal 實例置為了 null,但是在 Thread 類中依然有這個引用鏈的存在。
GC 在垃圾回收的時候會進行可達性分析,它會發(fā)現(xiàn)這個 ThreadLocal 對象依然是可達的,所以對于這個 ThreadLocal 對象不會進行垃圾回收,這樣的話就造成了內(nèi)存泄漏的情況。
JDK 開發(fā)者考慮到了這一點,所以 ThreadLocalMap 中的 Entry 繼承了 WeakReference 弱引用,代碼如下所示:
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
可以看到,這個 Entry 是 extends WeakReference。弱引用的特點是,如果這個對象只被弱引用關聯(lián),而沒有任何強引用關聯(lián),那么這個對象就可以被回收,所以弱引用不會阻止 GC。因此,這個弱引用的機制就避免了 ThreadLocal 的內(nèi)存泄露問題。
這就是為什么 Entry 的 key 要使用弱引用的原因。
Value 的泄漏
可是,如果我們繼續(xù)研究的話會發(fā)現(xiàn),雖然 ThreadLocalMap 的每個 Entry 都是一個對 key 的弱引用,但是這個 Entry 包含了一個對 value 的強引用,還是剛才那段代碼:
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
可以看到,value = v 這行代碼就代表了強引用的發(fā)生。
正常情況下,當線程終止,key 所對應的 value 是可以被正常垃圾回收的,因為沒有任何強引用存在了。但是有時線程的生命周期是很長的,如果線程遲遲不會終止,那么可能 ThreadLocal 以及它所對應的 value 早就不再有用了。在這種情況下,我們應該保證它們都能夠被正常的回收。
為了更好地分析這個問題,我們用下面這張圖來看一下具體的引用鏈路(實線代表強引用,虛線代表弱引用):
可以看到,左側(cè)是引用棧,棧里面有一個 ThreadLocal 的引用和一個線程的引用,右側(cè)是我們的堆,在堆中是對象的實例。
我們重點看一下下面這條鏈路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value實例。
這條鏈路是隨著線程的存在而一直存在的,如果線程執(zhí)行耗時任務而不停止,那么當垃圾回收進行可達性分析的時候,這個 Value 就是可達的,所以不會被回收。但是與此同時可能我們已經(jīng)完成了業(yè)務邏輯處理,不再需要這個 Value 了,此時也就發(fā)生了內(nèi)存泄漏問題。
JDK 同樣也考慮到了這個問題,在執(zhí)行 ThreadLocal 的 set、remove、rehash 等方法時,它都會掃描 key 為 null 的 Entry,如果發(fā)現(xiàn)某個 Entry 的 key 為 null,則代表它所對應的 value 也沒有作用了,所以它就會把對應的 value 置為 null,這樣,value 對象就可以被正?;厥樟恕?/p>
但是假設 ThreadLocal 已經(jīng)不被使用了,那么實際上 set、remove、rehash 方法也不會被調(diào)用,與此同時,如果這個線程又一直存活、不終止的話,那么剛才的那個調(diào)用鏈就一直存在,也就導致了 value 的內(nèi)存泄漏。
如何避免內(nèi)存泄露
分析完這個問題之后,該如何解決呢?解決方法就是我們本課時的標題:調(diào)用 ThreadLocal 的 remove 方法。調(diào)用這個方法就可以刪除對應的 value 對象,可以避免內(nèi)存泄漏。
我們來看一下 remove 方法的源碼:
- public void remove() {
- ThreadLocalMap m = getMap(Thread.currentThread());
- if (m != null)
- m.remove(this);
- }
可以看出,它是先獲取到 ThreadLocalMap 這個引用的,并且調(diào)用了它的 remove 方法。這里的 remove 方法可以把 key 所對應的 value 給清理掉,這樣一來,value 就可以被 GC 回收了。
所以,在使用完了 ThreadLocal 之后,我們應該手動去調(diào)用它的 remove 方法,目的是防止內(nèi)存泄漏的發(fā)生。