1. 程式人生 > 其它 >強引用、軟引用、弱引用、虛引用及ReferenceQueue的使用

強引用、軟引用、弱引用、虛引用及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