如果一個對象僅僅是偶爾使用,並且希望在使用時隨時就能獲取到,但又不想影響此對象的垃圾收集,那么你應該用 WeakReference 來引用該對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
private static final ReferenceQueue<TestClass> QUEUE = new ReferenceQueue<>(); WeakReference<TestClass> weakRef = new WeakReference<>(obj, QUEUE);
一般來說,很少直接使用WeakReference,而是使用WeakHashMap。在WeakHashMap中,內部有一個引用隊列,插入的元素會被包裹成WeakReference,並加入隊列中,用來做緩存再合適不過。
在Tomcat的緩存中,其實就用到了WeakHashMap:
public final class ConcurrentCache<K,V> { private final int size; private final Map<K,V> eden; private final Map<K,V> longterm; public ConcurrentCache(int size) { this.size = size; this.eden = new ConcurrentHashMap<>(size); this.longterm = new WeakHashMap<>(size); } public V get(K k) { // 先從eden中取 V v = this.eden.get(k); if (v == null) { // 如果取不到再從longterm中取 synchronized (longterm) { v = this.longterm.get(k); } // 如果取到則重新放到eden中 if (v != null) { this.eden.put(k, v); } } return v; } public void put(K k, V v) { if (this.eden.size() >= size) { // 如果eden中的元素數量大於指定容量,將所有元素放到longterm中 synchronized (longterm) { this.longterm.putAll(this.eden); } this.eden.clear(); } this.eden.put(k, v); } }
這里有eden和longterm的兩個map,如果對jvm堆了解的話,可以看出tomcat在這里是使用ConcurrentHashMap和WeakHashMap做了類似分代緩存的操作。
在put方法里,在插入鍵值對時,先檢查eden緩存的容量是否超出設定的大小。如果沒有則直接放入eden緩存,如果超了則鎖定longterm將eden中所有的鍵值對都放入longterm。再將eden清空並插入該鍵值對。
在get方法中,也是優先從eden中找對應的key,如果沒有則進入longterm緩存中查找,找到后就加入eden緩存並返回。
經過這樣的設計,相對常用的對象都能在eden緩存中找到,不常用(有可能被銷毀的對象)的則進入longterm緩存。而longterm的key的實際對象沒有其他引用指向它時,gc就會自動回收heap中該弱引用指向的實際對象,並將弱引用放入其引用隊列中。
弱引用與軟引用對比
弱引用與軟引用的區別在於:
- 只具有弱引用的對象擁有更短暫的生命周期。
- 被垃圾回收器回收的時機不一樣,在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。而被軟引用關聯的對象只有在內存不足時才會被回收。
- 弱引用不會影響GC,而軟引用會一定程度上對GC造成影響。
相似之處:都是用來描述非必需對象的。
那么什么時候用SoftReference,什么時候用WeakReference呢?
如果緩存的對象是比較大的對象,使用頻率相對較高的對象,那么使用SoftReference會更好,因為這樣能讓緩存對象有更長的生命周期。
如果緩存對象都是比較小的對象,使用頻率一般或者相對較低,那么使用WeakReference會更合適。
當然,如果實在不知道選哪個,一般而言,用作緩存時使用WeakHashMap都不會有太大問題
- 弱引用是比軟引用更弱的引用類型
- 弱引用不能延長對象的生命周期,一旦對象只剩下弱引用,它就隨時可能會被回收
- 可以通過弱引用獲取對象的強引用
- 弱引用適合用作緩存