Java中WeakHashMap的弱鍵回收機(jī)制
1. WeakHashMap介紹
WeakHashMap繼承AbstractMap,實(shí)現(xiàn)了Map接口。
和HashMap一樣,WeakHashMap也是一個(gè)散列表,它存儲(chǔ)的內(nèi)容也是鍵值對(duì)(key-value)映射,而且鍵和值都可以是null。
不過(guò)WeakHashMap的鍵是"弱鍵"。在WeakHashMap中,當(dāng)某個(gè)鍵不再正常使用時(shí),會(huì)被從WeakHashMap中被自動(dòng)移除。
更精確地說(shuō),對(duì)于一個(gè)給定的鍵,其映射的存在并不阻止垃圾回收器對(duì)該鍵的丟棄,這就使該鍵成為可終止的,被終止,然后被回收。某個(gè)鍵被終止時(shí),它對(duì)應(yīng)的鍵值對(duì)也就從映射中有效地移除了。
WeakHashMap內(nèi)部是通過(guò)弱引用來(lái)管理 entry 的,弱引用的特性對(duì)應(yīng)到WeakHashMap上意味著什么呢?
將一對(duì) key, value 放入到WeakHashMap里并不能避免該 key 值被GC回收,除非在WeakHashMap之外還有對(duì)該 key 的強(qiáng)引用。
和HashMap一樣,WeakHashMap是不同步的??梢允褂肅ollections.synchronizedMap方法來(lái)構(gòu)造同步的WeakHashMap。
2. WeakHashMap例子
public class TestWeakHashMap { public static void main(String[] args) { WeakHashMap<String, String> weakHashMap = new WeakHashMap<>(10); String key0 = new String("kuang"); String key1 = new String("zhong"); String key2 = new String("wen"); // 存放元素 weakHashMap.put(key0, "q1"); weakHashMap.put(key1, "q2"); weakHashMap.put(key2, "q3"); System.out.printf("weakHashMap: %s\n", weakHashMap); // 是否包含某key System.out.printf("contains key kuang : %s\n", weakHashMap.containsKey(key0)); System.out.printf("contains key zhong : %s\n", weakHashMap.containsKey(key1)); // 是否包含某value System.out.printf("contains value 0 : %s\n", weakHashMap.containsValue(0)); // 移除key weakHashMap.remove(key2); System.out.printf("weakHashMap after remove: %s", weakHashMap); // 這意味著"弱鍵"key0再?zèng)]有被其它對(duì)象引用,調(diào)用gc時(shí)會(huì)回收WeakHashMap中與key0對(duì)應(yīng)的鍵值對(duì) key0 = null; // 內(nèi)存回收,這里會(huì)回收WeakHashMap中與"key0"對(duì)應(yīng)的鍵值對(duì) System.gc(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 遍歷WeakHashMap Iterator iter = weakHashMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry en = (Map.Entry) iter.next(); System.out.printf("next : %s - %s\n", en.getKey(), en.getValue()); } // 打印WeakHashMap的實(shí)際大小 System.out.printf("after gc WeakHashMap size: %s\n", weakHashMap.size()); } }
執(zhí)行輸出:
weakHashMap: {wen=q3, zhong=q2, kuang=q1}
contains key kuang : true
contains key zhong : true
contains value 0 : false
weakHashMap after remove: {zhong=q2, kuang=q1}next : zhong - q2
after gc WeakHashMap size: 1
上面的例子展示了WeakHashMap的增刪改查,以及弱鍵的回收,可以看到把Key的引用置為null,gc后,會(huì)將該鍵值對(duì)回收。
3. WeakHashMap的使用場(chǎng)景
一般用做緩存,比如Tomcat的源碼里,實(shí)現(xiàn)緩存時(shí)會(huì)用到WeakHashMap,在緩存系統(tǒng)中,使用WeakHashMap可以避免內(nèi)存泄漏,但是使用WeakHashMap做緩存時(shí)要注意,如果只有它的key只有WeakHashMap本身在用,而在WeakHashMap之外沒(méi)有對(duì)該 key 的強(qiáng)引用,那么GC時(shí)會(huì)回收這個(gè)key對(duì)應(yīng)的entry。所以WeakHashMap不能用做主緩存,合適的用法應(yīng)該是用它做二級(jí)的內(nèi)存緩存,即那么過(guò)期緩存數(shù)據(jù)或者低頻緩存數(shù)據(jù)。
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) { V v = this.eden.get(k); if (v == null) { synchronized (longterm) { v = this.longterm.get(k); } if (v != null) { this.eden.put(k, v); } } return v; } public void put(K k, V v) { if (this.eden.size() >= size) { synchronized (longterm) { this.longterm.putAll(this.eden); } this.eden.clear(); } this.eden.put(k, v); } }
源碼中有eden和longterm的兩個(gè)map,對(duì)jvm堆區(qū)有所了解的話,可以猜測(cè)出tomcat在這里是使用ConcurrentHashMap和WeakHashMap做了分代的緩存。在put方法里,在插入一個(gè)k-v時(shí),先檢查eden緩存的容量是不是超了。沒(méi)有超就直接放入eden緩存,如果超了則鎖定longterm將eden中所有的k-v都放入longterm。再將eden清空并插入k-v。在get方法中,也是優(yōu)先從eden中找對(duì)應(yīng)的v,如果沒(méi)有則進(jìn)入longterm緩存中查找,找到后就加入eden緩存并返回。
經(jīng)過(guò)這樣的設(shè)計(jì),相對(duì)常用的對(duì)象都能在eden緩存中找到,不常用(有可能被銷(xiāo)毀的對(duì)象)的則進(jìn)入longterm緩存。而longterm的key的實(shí)際對(duì)象沒(méi)有其他引用指向它時(shí),gc就會(huì)自動(dòng)回收heap中該弱引用指向的實(shí)際對(duì)象,弱引用進(jìn)入引用隊(duì)列。longterm調(diào)用expungeStaleEntries()方法,遍歷引用隊(duì)列中的弱引用,并清除對(duì)應(yīng)的Entry,不會(huì)造成內(nèi)存空間的浪費(fèi)。
4. WeakHashMap的數(shù)據(jù)結(jié)構(gòu)
前面已經(jīng)大概了解了WeakHashMap,接下來(lái)來(lái)分析WeakHashMap的源碼,先從它的數(shù)據(jù)結(jié)構(gòu)開(kāi)始。
4.1 類(lèi)的定義
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> { }
java.lang.Object ? java.util.AbstractMap<K, V> ? java.util.WeakHashMap<K, V>
4.2 變量與常量
private static final int DEFAULT_INITIAL_CAPACITY = 16; private static final int MAXIMUM_CAPACITY = 1 << 30; private static final float DEFAULT_LOAD_FACTOR = 0.75f; Entry<K,V>[] table; private int size; private int threshold; private final float loadFactor; private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); int modCount;
- DEFAULT_INITIAL_CAPACITY : 初始容量
- MAXIMUM_CAPACITY : 最大容量
- DEFAULT_LOAD_FACTOR : 默認(rèn)加載因子
- table : Entry數(shù)組
- size : 實(shí)際存放的數(shù)據(jù)個(gè)數(shù)
- threshold : 擴(kuò)容閾值
- loadFactor : 加載因子
- queue : 引用隊(duì)列
- modCount : 修改次數(shù)
4.3 Entry類(lèi)
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; 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; } @SuppressWarnings("unchecked") public K getKey() { return (K) WeakHashMap.unmaskNull(get()); } public V getValue() { return value; } public V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } ... }
可以看到Entry類(lèi)實(shí)現(xiàn)了Map.Entry接口,繼承弱引用(WeakReference),屬性有key,value和next引用。
構(gòu)造器中需要傳入一個(gè)引用隊(duì)列,方法主要看getKey():
return (K) WeakHashMap.unmaskNull(get());
再來(lái)看看WeakHashMap的靜態(tài)方法unmaskNull():
private static final Object NULL_KEY = new Object(); static Object unmaskNull(Object key) { return (key == NULL_KEY) ? null : key; }
判斷key是否等于NULL_KEY來(lái)選擇是否返回null。
4.4 類(lèi)關(guān)系圖
5. WeakHashMap的弱鍵回收
上面看完WeakHashMap的數(shù)據(jù)結(jié)構(gòu),那么WeakHashMap是如何實(shí)現(xiàn)弱鍵回收的呢?其實(shí)根據(jù)前面的文章也能猜到,利用Reference和ReferenceQueue。
5.1 put數(shù)據(jù)
public V put(K key, V value) { Object k = maskNull(key); int h = hash(k); Entry<K,V>[] tab = getTable(); int i = indexFor(h, tab.length); for (Entry<K,V> e = tab[i]; e != null; e = e.next) { if (h == e.hash && eq(k, e.get())) { V oldValue = e.value; if (value != oldValue) e.value = value; return oldValue; } } modCount++; Entry<K,V> e = tab[i]; tab[i] = new Entry<>(k, value, queue, h, e); if (++size >= threshold) resize(tab.length * 2); return null; } private static Object maskNull(Object key) { return (key == null) ? NULL_KEY : key; }
- 判斷key是否為null,為null的話將key賦值為NULL_KEY。
- 計(jì)算key的hash值,然后根據(jù)hash值查找待插入的位置。
- 遍歷Entry數(shù)組,看該鍵是否已存在,存在的話則替換舊值,并返回舊值。
- 不存在則構(gòu)建Entry對(duì)象存入數(shù)組。
這個(gè)流程和HashMap,HashTable等差不多。
5.2 get數(shù)據(jù)
public V get(Object key) { Object k = maskNull(key); int h = hash(k); Entry<K,V>[] tab = getTable(); int index = indexFor(h, tab.length); Entry<K,V> e = tab[index]; while (e != null) { if (e.hash == h && eq(k, e.get())) return e.value; e = e.next; } return null; }
一句話,先根據(jù)key的hash值找到索引位置,然后拿到Entry對(duì)象,再判斷該Entry是否存在下一個(gè)引用(即hash碰撞),遍歷該單鏈表,比較value值。
5.3 弱鍵如何回收?
根據(jù)上面的分析就很容易得出了,WeakHashMap內(nèi)部的數(shù)據(jù)存儲(chǔ)是用Entry[]數(shù)組,即鍵值對(duì)數(shù)組。
Entry類(lèi)繼承于WeakReferece(弱引用),弱引用的特點(diǎn)再重申下:當(dāng)JVM進(jìn)行垃圾回收時(shí),無(wú)論內(nèi)存是否充足,都會(huì)回收被弱引用關(guān)聯(lián)的對(duì)象。
在構(gòu)造一個(gè)Entry對(duì)象的時(shí)候,會(huì)傳入一個(gè)ReferenceQueue,key為弱引用包裹的對(duì)象:
public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); }
再來(lái)看看WeakHashMap如何清理已經(jīng)被回收的key的,被回收的key會(huì)存放在引用隊(duì)列中:
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; } } } }
遍歷引用隊(duì)列,然后刪除已被回收的鍵值對(duì)(從數(shù)組移除,改變單鏈表結(jié)點(diǎn)引用,將value賦值為null),該方法會(huì)在WeakHashMap增刪改查、擴(kuò)容的地方調(diào)用。
因?yàn)橐粋€(gè)key-value存放到WeakHashMap中后,key會(huì)被用弱引用包起來(lái)存儲(chǔ),如果這個(gè)key在WeakHashMap外部沒(méi)有強(qiáng)引用的話,GC時(shí)將被回收,然后WeakHashMap根據(jù)引用隊(duì)列對(duì)已回收的key做清理。
到此這篇關(guān)于Java中WeakHashMap的弱鍵回收機(jī)制的文章就介紹到這了,更多相關(guān)WeakHashMap弱鍵回收內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java版數(shù)據(jù)結(jié)構(gòu)插入數(shù)據(jù)時(shí)遇到的結(jié)點(diǎn)為空的問(wèn)題詳解
這篇文章主要介紹了Java版數(shù)據(jù)結(jié)構(gòu)插入數(shù)據(jù)時(shí)遇到的結(jié)點(diǎn)為空的問(wèn)題及解決辦法,需要的朋友們可以學(xué)習(xí)下。2019-09-09java學(xué)生信息管理系統(tǒng)MVC架構(gòu)詳解
這篇文章主要為大家詳細(xì)介紹了java學(xué)生信息管理系統(tǒng)MVC架構(gòu)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11java播放聲音類(lèi)和一個(gè)簡(jiǎn)單示例
這篇文章主要介紹了一個(gè)java播放聲音類(lèi)和一個(gè)java播放聲音的應(yīng)用程序,應(yīng)用程序可以單次播放聲音、循環(huán)播放聲音,需要的朋友可以參考下2014-03-03微服務(wù)之間如何通過(guò)feign調(diào)用接口上傳文件
這篇文章主要介紹了微服務(wù)之間如何通過(guò)feign調(diào)用接口上傳文件的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Java實(shí)現(xiàn)常用加密算法——單向加密算法MD5和SHA
本篇文章主要介紹了Java實(shí)現(xiàn)常用加密算法——單向加密算法MD5和SHA,信息加密后數(shù)據(jù)更安全,需要的朋友可以參考下。2016-10-10Java實(shí)現(xiàn)AWT四大事件的詳細(xì)過(guò)程
AWT的事件處理是一種委派式事件處理方式:普通組件(事件源)將整個(gè)事件處理委托給特定的對(duì)象(事件監(jiān)聽(tīng)器);當(dāng)該事件源發(fā)生指定的事件時(shí),就通知所委托的事件監(jiān)聽(tīng)器,由事件監(jiān)聽(tīng)器來(lái)處理這個(gè)事件2022-04-04JavaMe開(kāi)發(fā)繪制可自動(dòng)換行文本
JavaMe Graphics類(lèi)中的drawString不支持文本換行,這樣繪制比較長(zhǎng)的字符串時(shí),文本被繪制在同一行,超過(guò)屏幕部分的字符串被截?cái)嗔恕H绾问估L制的文本能自動(dòng)換行呢?2015-09-09JAVA使用SimpleDateFormat類(lèi)表示時(shí)間代碼實(shí)例
這篇文章主要介紹了JAVA使用SimpleDateFormat類(lèi)表示時(shí)間代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04