WeakHashMap和ThreadLocal內存泄漏中的弱引用運行原理


本文原創,如有引用,請指明出處。

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().
}

本文原創,如有引用,請指明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM