本文原創,如有引用,請指明出處。
WeakHashMap和ThreadLocal內存泄漏中的弱引用運行原理
WeakHashMap的內存泄漏問題
DefaultChannelPipeline中使用了WeakHashMap來作為緩存。事實上,WeakHashMap的設計理念與ThreadLocal很像。但是ThreadLocal重新設計了自己的實現,並沒有直接使用WeakHashMap。同時,ThreadLocal存在着內存泄漏的問題。而網上關於WeakHashMap內存泄漏問題卻談的非常少。下面貼一個網上關於WeakHashMap內存泄漏問題的代碼。出處在這里對WeakHashMap的使用不慎導致內存溢出分析
import java.util.WeakHashMap;
public class WeekTest {
public static class Locker {
private static WeakHashMap<String, Locker> lockerMap = new WeakHashMap<String, Locker>();
private final String id;
private Locker(String id) {
this.id= id;
}
public synchronized static Locker acquire(String id) {
Locker locker = lockerMap.get(id);
if (locker == null) {
locker = new Locker(id);
//lockerMap.put(id, locker); //問題代碼,導致了entry.key == entry.value.id
lockerMap.put(new String(id), locker); //這是一種修改方式,保證了WeakHashMap中的key,沒有被value直接或間接所引用
}
return locker;
}
public String getId() {
return this.id;
}
public static int getSize() {
return lockerMap.size();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10000000; i++) {
Locker.acquire("abc" + i);
if (i % 10000 == 0) {
System.gc();
System.out.println(Locker.getSize()); //輸出垃圾回收后的Map的Size
}
}
}
}
讀者可以自己運行上述代碼。比較結果,很容易發現內存泄漏了。事實上,這里出現了許多疑問。當一個對象僅僅被弱引用指定時,它將被回收。因此,它被用來設計ThreadLocalMap和WeakHashMap。二者原理十分類似。二者內部數據結構十分類似。ThreadLocalMap元數據設計Entry代碼如下:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
下面是WeakHashMap的源代碼。這里只比較數據結構,會發現兩個內部類十分相似。但是,ThreadLocal會引發內存泄漏,而WeakHashMap很少引發內存泄漏。除非使用錯誤,WeakHashMap中的key被value直接或間接所引用。在使用正確的情況下,WeakHashMap中的數據在key沒有被強引用的情況下,回收器可以正確回收整個Entry的內存;ThreadLocal則必須在當前線程停止后才可以,否則回收器將僅僅回收key(Threshold)內存,value內存無法被回收。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
//其他忽略
弱引用運行原理
為了理解這其中原因,我們需要查看三個類WeakReference,Reference,ReferenceQueue。WeakReference繼承了Reference,
public class WeakReference<T> extends Reference<T> {
/**
* Creates a new weak reference that refers to the given object. The new
* reference is not registered with any queue.
*
* @param referent object the new weak reference will refer to
*/
public WeakReference(T referent) {
super(referent);
}
/**
* Creates a new weak reference that refers to the given object and is
* registered with the given queue.
*
* @param referent object the new weak reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
整個WeakReference的代碼十分簡單。主要的邏輯由Reference實現。WeakReference僅實現了兩個構造器。Reference的實現較晦澀,但原理十分簡單。我們將結合例子講解。如果我們想實現一個字符串變成弱引用。實現如下:
String String = new String("Random"); // 強引用
ReferenceQueue<String> StringQ = new ReferenceQueue<String>();// 引用隊列
WeakReference<String> StringWRefernce = new WeakReference<String>(String, StringQ);//弱引用
構造器有兩個十分重要的屬性。這里構造器第一個參數是new String("Random")。在Reference中,回收器線程將對這個對象進行監視,從而決定是否將Reference放入ReferenceQueue中。而這個ReferenceQueue正是傳遞給構造器的第二個屬性。Reference對象為了自己專門定義了四個內部狀態:Active-Pending-Enqueued-Inactive。當它被傳進來那一刻,Reference對象的狀態為Active。回收器開始運作。當可達性發生變化,Reference對象狀態將被轉化為Pending(這部分是由JVM實現的)。同時,Reference對象將被加入一個Pending隊列(源碼中中實現為鏈表)。Reference類還啟動一個守護線程ReferenceHandle。這個線程負責將Pending隊列中的Reference對象加入ReferenceQueue隊列中。可以說ReferenceQueue中的Reference對象已經是無用對象。ReferenceQueue可以看作是一個Reference鏈表。同時加入其中的Reference對象已經不可達,自動回收。示例代碼如下:(改自Reference Queues)
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
public class WeekTest {
public static <T>boolean checkNotNull(T t,String fieldName){
try {
Field field=t.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object result=field.get(t);
return result!=null;
} catch (NoSuchFieldException e) {
System.out.println("不存在該字段");
e.printStackTrace();
} catch (IllegalAccessException e) {
System.out.println("");
e.printStackTrace();
}
return false;
}
public static void main(String[] args) throws InterruptedException {
String String = new String("Random"); // a strong object
ReferenceQueue<String> StringQ = new ReferenceQueue<String>();// the ReferenceQueue
WeakReference<String> StringWRefernce = new WeakReference<String>(String, StringQ);
System.out.println("String created as a weak ref " + StringWRefernce);
System.out.println("Is refQ head not null?"+checkNotNull(StringQ,"head"));
Runtime.getRuntime().gc();
System.out.println("Any weak references in Q ? " + (StringQ.poll() != null));
String = null; // the only strong reference has been removed. The heap
// object is now only weakly reachable
System.out.println("Now to call gc...");
Runtime.getRuntime().gc(); // the object will be cleared here - finalize will be called.
System.out.println("Is refQ head not null?"+checkNotNull(StringQ,"head"));
//這里改用remove()方法是因為poll()是無阻塞方法。一個對象標記不可達到加入ReferenceQueue是分別由GC和線程ReferenceHandler分別來實現的,因此是存在延遲的。而remove()是阻塞方法。
System.out.println("Any weak references in Q ? " + (StringQ.remove() != null));
System.out.println("Does the weak reference still hold the heap object ? " + (StringWRefernce.get() != null));
System.out.println("Is the weak reference added to the ReferenceQ ? " + (StringWRefernce.isEnqueued()));
}
}
這里我們回到ThreadLocal中。ThreadLocal中的內部類Entry繼承了WeakReference,傳遞給Reference監視的對象是ThreadLocal。value則是Entry內部的強引用。當外部ThreadLocal被設置為空時,回收器設置Reference為Pending,再加入ReferenceQueue,最后被回收。至始至終,我們傳遞給JVM監視並准備回收的都是ThreadLocal。Entry中的value並沒有得到處理。因此如果線程一直沒結束,那么就會存在Thread->ThreadLocalMap->Entry(null,value)的引用,造成內存泄漏。
為什么WeakHashMap能夠回收value內存?很簡單,WeakHashMap專門實現了一個方法
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
WeakHashMap專門去翻找了ReferenceQueue。當發現了Reference中有某個key,說明它將被回收。那么e.value = null。直接將value置空,這樣value也可以進行回收。
以上就是整個弱引用的運行原理。如果有興趣,可以自己翻看jdk代碼,對照本文。如果本文有誤,歡迎指正。
補充
最后貼一個ReferenceQueue的使用代碼
ReferenceQueue<Foo> fooQueue = new ReferenceQueue<Foo>();
class ReferenceWithCleanup extends WeakReference<Foo> {
Bar bar;
ReferenceWithCleanup(Foo foo, Bar bar) {
super(foo, fooQueue);
this.bar = bar;
}
public void cleanUp() {
bar.cleanUp();
}
}
public Thread cleanupThread = new Thread() {
public void run() {
while(true) {
ReferenceWithCleanup ref = (ReferenceWithCleanup)fooQueue.remove();
ref.cleanUp();
}
}
}
public void doStuff() {
cleanupThread.start();
Foo foo = new Foo();
Bar bar = new Bar();
ReferenceWithCleanup ref = new ReferenceWithCleanup(foo, bar);
... // From now on, once you release all non-weak references to foo,
// then at some indeterminate point in the future, bar.cleanUp() will
// be run. You can force it by calling ref.enqueue().
}