強引用、軟引用、弱引用、虛引用及ReferenceQueue的使用

何爲ReferenceQueue

在java的引用體系中,存在着強引用,軟引用,虛引用,幽靈引用,這4種引用類型。在正常的使用過程中,我們定義的類型都是強引用的,這種引用類型在回收中,只有當其它對象沒有對這個對象的引用時,纔會被GC回收掉。簡單來說,對於以下定義:

Object obj = new Object();
Ref ref = new Ref(obj);

在這種情況下,如果ref沒有被GC,那麼obj這個對象肯定不會GC的。因爲ref引用到了obj。如果obj是一個大對象呢,多個這種對象的話,應用肯定一會就掛掉了。

那麼,如果我們希望在這個體系中,如果obj沒有被其它對象引用,只是在這個Ref中存在引用時,就把obj對象gc掉。這時候就可以使用這裏提到的Reference對象了。

我們希望當一個對象被gc掉的時候通知用戶線程,進行額外的處理時,就需要使用引用隊列了。ReferenceQueue即這樣的一個對象,當一個obj被gc掉之後,其相應的包裝類,即ref對象會被放入queue中。我們可以從queue中獲取到相應的對象信息,同時進行額外的處理。比如反向操作,數據清理等。

使用隊列進行數據監控

一個簡單的例子,通過往map中放入10000個對象,每個對象大小爲1M字節數組。使用引用隊列監控被放入的key的回收情況。代碼如下所示:

Object value = new Object();
Map<Object, Object> map = new HashMap<>();
for(int i = 0;i < 10000;i++) {
    byte[] bytes = new byte[_1M];
    WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);
    map.put(weakReference, value);
}
System.out.println("map.size->" + map.size());

這裏使用了weakReference對象,即當值不再被引用時,相應的數據被回收。另外使用一個線程不斷地從隊列中獲取被gc的數據,代碼如下:

Thread thread = new Thread(() -> {
    try {
        int cnt = 0;
        WeakReference<byte[]> k;
        while((k = (WeakReference) referenceQueue.remove()) != null) {
            System.out.println((cnt++) + "回收了:" + k);
        }
    } catch(InterruptedException e) {
        //結束循環
    }
});
thread.setDaemon(true);
thread.start();

結果如下所示:

9992回收了:java.lang.ref.WeakReference@1d13cd4
9993回收了:java.lang.ref.WeakReference@118b73a
9994回收了:java.lang.ref.WeakReference@1865933
9995回收了:java.lang.ref.WeakReference@ad82c
map.size->10000

在這次處理中,map並沒有因爲不斷加入的1M對象由產生OOM異常,並且最終運行結果之後map中的確有1萬個對象。表示確實被放入了相應的對象信息。不過其中的key(即weakReference)對象中的byte[]對象卻被回收了。即不斷new出來的1M數組被gc掉了。

從命令行中,我們看到有9995個對象被gc,即意味着在map的key中,除了weakReference之外,沒有我們想要的業務對象。那麼在這樣的情況下,是否意味着這9995個entry,我們認爲就是沒有任何意義的對象,那麼是否可以將其移除掉呢。同時還期望size值可以打印出5,而不是10000.
WeakHashMap就是這樣的一個類似實現。

在類weakHashMap中的使用

weakHashMap即使用weakReference當作key來進行數據的存儲,當key中的引用被gc掉之後,它會自動(類似自動)的方式將相應的entry給移除掉,即我們會看到size發生了變化。

從簡單來看,我們認爲其中所有一個類似的機制從queue中獲取引用信息,從而使得被gc掉的key值所對應的entry從map中被移除。這個處理點就在我們調用weakhashmap的各個處理點中,比如get,size,put等。簡單點來說,就是在調用get時,weakHashMap會先處理被gc掉的key值,然後再處理我們的業務調用。

簡單點代碼如下:

public int size() {
    if (size == 0)
        return 0;
    expungeStaleEntries();
    return size;
}

此處的expungeStaleEntries即移除方法,具體的邏輯可以由以下的流程來描述:

  • A:使用一個繼承於WeakReference的entry對象表示每一個kv對,其中的原引用對象即我們在放入map中的key值
  • B:爲保證效率以及儘可能的不使用key值,hash經過預先計算。這樣在定位數據及重新get時不再需要使用原引用對象
  • C:由queue拿到的事件對象,即這裏的entry值。通過entry定位到具體的桶位置,通過鏈表計算將entry的前後重新連接起來(即p.pre.next = p.next)

因此,這裏的引用處理並不是自動的,其實是我們在調用某些方法的時候處理,所以我們認爲它不是一種自動的,只是表面上看起來是這種處理。
具體的代碼,即將開始的map定義爲一個WeakHashMap,最終的輸出類似如下所示:

9993回收了:java.lang.ref.WeakReference@12aa816
9994回收了:java.lang.ref.WeakReference@2bd967
9995回收了:java.lang.ref.WeakReference@13e9593
weakHashMap.size->4

在上面的代碼中,由於weakhashmap不允許自定義queue,所以上面的監控是針對value的。在weakHashMap中,queue在weakhashmap在內部定義,並且由內部消化使用了。如果我們在自己進一步處理,那就只能自定義類似weakHashMap實現,或者使用反向操作。即在監控到變化之後,自己處理map的kv。

隊列監控的反向操作

反向操作,即意味着一個數據變化了,可以通過weakReference對象反向拿相關的數據,從而進行業務的處理。比如,我們可以通過繼承weakReference對象,加入自定義的字段值,額外處理。一個類似weakHashMap如下,這時,我們不再將key值作爲弱引用處理,而是封裝在weakReference對象中,以實現額外的處理。

WeakR對象定義如下:

//描述一種強key關係的處理,當value值被回收之後,我們可以通過反向引用將key從map中移除的做法
//即通過在weakReference中加入其所引用的key值,以獲取key信息,再反向移除map信息
class WeakR extends WeakReference<byte[]> {
    private Object key;
    WeakR(Object key, byte[] referent, ReferenceQueue<? super byte[]> q) {
        super(referent, q);
        this.key = key;
    }
}

那麼,相應的map,我們就使用普通的hashMap,將weakR作爲value進行存儲,如下所示:

final Map<Object, WeakR> hashMap = new HashMap<>();
for(int i = 0;i < 10000;i++) {
    byte[] bytesKey = new byte[_1M];
    byte[] bytesValue = new byte[_1M];
    hashMap.put(bytesKey, new WeakR(bytesKey, bytesValue, referenceQueue));
}

相應的隊列,我們則一樣地進行監控,不同的是,我們對獲取的WeakR對象進行了額外的處理,如下所示:

int cnt = 0;
WeakR k;
while((k = (WeakR) referenceQueue.remove()) != null) {
    System.out.println((cnt++) + "回收了:" + k);
    //觸發反向hash remove
    hashMap.remove(k.key);
    //額外對key對象作其它處理,比如關閉流,通知操作等
}

其實就是拿到反向引用的key值(這裏的value已經不存在了),因爲kv映射已沒有意義,將其從map中移除掉。同時,我們還可以作其它的操作(具體的操作還沒想到,嘿嘿)

這個也可以理解爲就是一個類似cache的實現。
在cache中,key不重要並且通常都很少,value纔是需要對待的。這裏通過監控value變化,反向修改map,以達到控制kv的目的,避免出現無用的kv映射。

相應的輸出,如下所示:

9995回收了:com.m_ylf.study.java.reference.TestCase$1WeakR@13c5f83
9996回收了:com.m_ylf.study.java.reference.TestCase$1WeakR@197558c
9997回收了:com.m_ylf.study.java.reference.TestCase$1WeakR@164bc7e
hashMap.size->1

在Google Guava的簡單描述

在google guava中,實現了一個類似在第4中所對應的操作。同時對於反向操作,通過繼承一個指定的對象(可以理解爲weakReference和callback的組合對象),當value值被gc之後,即可以直接在回調中處理業務即可,不需要自己來監控queue。(見FinalizableReference)

原文地址:https://www.iflym.com/index.php/java-programe/201407140001.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章