內(nèi)存泄漏(Memory Leak)是指程序中已動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費摄欲,導(dǎo)致程序運行速度減慢甚至系統(tǒng)崩潰等嚴重后果。 ——百度百科
上述的意思用在 java 中就是存在已經(jīng)沒有任何引用的對象疮薇,但是 GC 又不能把對象所在的內(nèi)存回收掉胸墙,所以就造成了內(nèi)存泄漏。
我們知道ThreadLocal 主要解決的是對象不能被多個線程同時訪問的問題按咒。根據(jù) ThreadLocal 的源碼看看它是怎么實現(xiàn)的迟隅。
ThreadLocal 設(shè)置數(shù)據(jù)的set()
方法
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);
}
可以看到在使用 ThreadLocal 設(shè)置數(shù)據(jù)時,其實設(shè)置到的是當前線程的 threadLocals 字段里,去 Thread 里看一看 threadLocals 變量
ThreadLocal.ThreadLocalMap threadLocals = null;
threadLocals 的類型是 ThreadLocal 里的內(nèi)部類 ThreadLocalMap玻淑,ThreadLocalMap 的中用來存儲數(shù)據(jù)的又是一個內(nèi)部類是Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
的 key 是當前 ThreadLocal嗽冒,value 值是我們要設(shè)置的數(shù)據(jù)。
WeakReference
表示的是弱引用补履,當 JVM 進行 GC 時添坊,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內(nèi)存空間是否足夠箫锤,都會回收它的內(nèi)存贬蛙。
因為 WeakReference<ThreadLocal<?>>
,所以在Entry
中ThreadLocal
是弱引用谚攒,一旦發(fā)生 GC阳准,ThreadLocal
會被 GC 回收掉,但是value
是強引用馏臭,它不會被回收掉野蝇。用一張圖來表示一下
圖中實線表示的是強引用,虛線表示的是弱引用括儒。
當JVM發(fā)生GC后绕沈,虛線會斷開應(yīng)用,也就是key會變?yōu)閚ull帮寻,value是強引用不會為null乍狐,整個Entry也不為null,它依然在ThreadLocalMap中固逗,并占據(jù)著內(nèi)存浅蚪,
我們獲取數(shù)據(jù)時,使用ThreadLocal的get()
方法烫罩,ThreadLocal并不為null惜傲,所以我們無法通過一個key為null去訪問到該entry的value。這就造成了內(nèi)存泄漏贝攒。
既然用弱引用會造成內(nèi)存泄漏操漠,直接用強引用可以么?
答案是不行饿这。如果是強引用的話浊伙,看看下面代碼
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(new Object());
threadLocal = null;
我們在設(shè)置完數(shù)據(jù)后,直接將threadLocal設(shè)為null长捧,這時棧中ThreadLocal Ref
到堆中ThreadLocal
斷開了嚣鄙,但是key
到ThreadLocal
的引用依然存在,GC依舊沒法回收串结,同樣會造成內(nèi)存泄漏哑子。
那弱引用比強引用好在哪舅列?
當key為弱引用時,同樣是上面代碼卧蜓,當threadLocal設(shè)為null時帐要,棧中ThreadLocal Ref
到堆中ThreadLoacl
斷開了,key
到ThreadLoacl
也因為GC斷開了弥奸,這時ThreadLocal
就可以被回收了榨惠。
同時,ThreadLocal也可以根據(jù)key.get() == null
來判斷key是否已經(jīng)被回收,因此ThreadLocal可以自己清理這些過期的節(jié)點來避免內(nèi)存泄漏盛霎。
其實赠橙,ThreadLocal做了很大的工作清除過期的key來避免發(fā)生內(nèi)存泄漏
-
在調(diào)用
set()
方法時,會進行清理private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } // 當key為null時愤炸,替換掉 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // 清理一些槽位期揪,清理過期key if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
1、 當key為null時规个,說明該位置被GC回收了凤薛,會將當前位置覆蓋掉。
2诞仓、 在
set()
方法最后調(diào)用了cleanSomeSlots()
中還會有清理的操作缤苫。看一看cleanSomeSlots()
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; // 真正的清理工作 i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
cleanSomeSlots()
中當判斷e != null && e.get() == null
為true時狂芋,說明已經(jīng)被GC回收了榨馁,會調(diào)用expungeStaleEntry()
進行清理工作憨栽,具體的邏輯就不再看了帜矾。
-
在調(diào)用
get()
方法時,如果沒有命中屑柔,會向后查找屡萤,也會進行清理操作private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else // 沒有命中向后查找 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) // 當key為null,說明被GC回收了掸宛,進行清理的操作 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
-
調(diào)用
remove()
時死陆,除了清理當前節(jié)點,還會向后進行清理操作private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); // 向后查找唧瘾,進行清理操作 expungeStaleEntry(i); return; } } }