Java弱引用與WeakHashMap

在《Effective Java 2nd Edition》中,第6條「消除過時的對象引用」提到,雖然Java有 垃圾回收機制,可是隻要是本身管理的內存,就應該警戒內存泄露的問題,例如的對象池、緩存中的過時對象都有可能引起內存泄露的問題。書中還提到能夠用 WeakHashMap來做爲緩存的容器能夠有效解決這一問題。以前也確實遇到過相似問題,可是沒有接觸過「弱引用」相關的問題,因而查閱了一些資料。    《Java 理論與實踐: 用弱引用堵住內存泄漏》一文也指出了使用全局的Map做爲緩存容器時發生的內存泄露問題,介紹瞭如何使用hprof工具來找出內存泄露,並分析瞭如何使用 弱引用來防止內存泄露,還分析了WeakHashMap的關鍵代碼,很是有參考價值。可是這篇文章遺漏了幾個很重要的須要注意的地方,也缺乏一段實驗代 碼,本文將會作出適當補充。
    一、四種引用
    從JDK1.2版本開始,把對象的引用分爲四種級別,從而使程序能更加靈活的控制對象的生命週期。這四種級別由高到低依次爲:強引用、軟引用、弱引用和虛引用。html

http://docs.oracle.com/javase/7/docs/api/java/lang/ref/Reference.html
    強引用:平時咱們編程的時候例如:Object object=new Object();那object就是一個強引用了。若是一個對象具備強引用,那就相似於必不可少的生活用品,垃圾回收器毫不會回收它。當內存空 間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足問題。
    軟引用(SoftReference):若是一個對象只具備軟引用,那就相似於可有可物的生活用品。若是內存空間足夠,垃圾回收器就不會回收它,若是內存 空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。 軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收,Java虛擬機就會把這個軟引用加入到與之關聯 的引用隊列中。java

http://docs.oracle.com/javase/7/docs/api/java/lang/ref/SoftReference.html
    弱引用(WeakReference):若是一個對象只具備弱引用,那就相似於可有可物的生活用品。弱引用與軟引用的區別在於:只具備弱引用的對象擁有更 短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程, 所以不必定會很快發現那些只具備弱引用的對象。  弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯 的引用隊列中。編程

http://docs.oracle.com/javase/7/docs/api/java/lang/ref/WeakReference.html
    虛引用(PhantomReference):「虛引用」顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象 僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。 虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之 關聯的引用隊列中。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序若是發現某個虛引用已經被加入到引用隊 列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。api

http://docs.oracle.com/javase/7/docs/api/java/lang/ref/PhantomReference.html
    二、WeakHashMap源碼分析
    WeakHashMap維護了一個ReferenceQueue,保存了全部存在引用的Key對象。
    private final ReferenceQueue<K> queue = new ReferenceQueue<K>();
    WeakHashMap.Entry<K,V>中並無保存Key,只是將Key與ReferenceQueue關聯上了。
    private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> {
    private V value;
    private final int hash;
    private Entry<K,V> next;
    Entry(K key, V value, ReferenceQueue<K> queue, int hash, Entry<K,V> next) {
    super(key, queue);
    this.value = value;
    this.hash  = hash;
    this.next  = next;
    }
    ……
    }
    WeakHashMap中有一個私有的expungeStaleEntries()方法,會在大部分公有方法中被調用。這個方法會將ReferenceQueue中全部失效的引用從Map中去除。
    private void expungeStaleEntries() {
    Entry<K,V> e;
    while ( (e = (Entry<K,V>) queue.poll()) != null) {
    int h = e.hash;
    int i = indexFor(h, 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;
    e.next = null;  // Help GC
    e.value = null; //  "   "
    size--;
    break;
    }
    prev = p;
    p = next;
    }
    }
    }
    三、幾個須要注意的地方
    WeakHashMap的Key是弱引用,Value不是。
    WeakHashMap不會自動釋放失效的弱引用,僅當包含了expungeStaleEntries()的公有方法被調用的時候纔會釋放。
    四、一個簡單的例子
    public static void main(String args[]) {
    WeakHashMap<String, String> map = new WeakHashMap<String, String>();緩存

    //new運算符構建一個新的對象是在運行期,因此該對象並不存儲在常量區。new運算符建立的對象不是放在常量區的。oracle

    map.put(new String("1"), "1");
    map.put("2", "2");  
    String s = new String("3");
    map.put(s, "3");
    while (map.size() > 0) {
    try {
    Thread.sleep(500);
    } catch (InterruptedException ignored) {
    }
    System.out.println("Map Size:"+map.size());
    System.out.println(map.get("1"));
    System.out.println(map.get("2"));
    System.out.println(map.get("3"));
    System.gc();
    }
    }
    運行結果是:
    Map Size:3
    1
    2
    3
    Map Size:2
    null
    2
    3
    Map Size:2
    null  //value中的「1」和new運算符後的1不是同一個對象,
    2
    3
    (一直循環)
    要注意String的特殊性,「2」是被放在常量池中的,因此沒有被回收。並非由於k,v中「2」用的是同一個對象。工具

----------------------------源碼分析

關於JVM中運行時常量池(見java虛擬機規範2.5.5節)
運行時常量池(Runtime Constant Pool)是每個類或接口的常量池(Constant_Pool,
§ 4.4)的運行時表示形式,它包括了若干種不一樣的常量:從編譯期可知的數值字面量到必須運行
期解析後才能得到的方法或字段引用。
運行時常量池扮演了相似傳統語言中符號表(Symbol
Table)的角色,不過它存儲數據範圍比一般意義上的符號表要更爲普遍。
每個運行時常量池都分配在 Java 虛擬機的方法區之中( § 2.5.4),在類和接口被加載到
虛擬機後,對應的運行時常量池就被建立出來。
在建立類和接口的運行時常量池時,可能會發生以下異常狀況:
 當建立類或接口的時候,若是構造運行時常量池所須要的內存空間超過了方法區所能提供的最
大值,那 Java 虛擬機將會拋出一個 OutOfMemoryError 異常。
關於構造運行時常量池的詳細信息,能夠參考「第 5 章 加載、 連接和初始化」的內容。
 this

相關文章
相關標籤/搜索