Java中WeakHashMap的工作原理詳解
從名字可以得知主要和Map有關(guān),不過還有一個Weak,我們就更能自然而然的想到這里面還牽扯到一種弱引用結(jié)構(gòu),因此想要徹底搞懂,我們還需要知道四種引用,需要的朋友可以參考下
一、什么是 WeakHashMap?
從名字可以得知主要和Map有關(guān),不過還有一個Weak,我們就更能自然而然的想到這里面還牽扯到一種弱引用結(jié)構(gòu),因此想要徹底搞懂,我們還需要知道四種引用。
強(qiáng)引用: 如果一個對象具有強(qiáng)引用,它就不會被垃圾回收器回收。即使當(dāng)前內(nèi)存空間不足,JVM也不會回收它,而是拋出 OutOfMemoryError 錯誤,使程序異常終止。 比如 String str = "hello"
這時候str就是一個強(qiáng)引用。
軟引用: 內(nèi)存足夠的時候,軟引用對象不會被回收,只有在內(nèi)存不足時,系統(tǒng)則會回收軟引用對象,如果回收了軟引用對象之后仍然沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。
弱引用: 如果一個對象具有弱引用,在垃圾回收時候,一旦發(fā)現(xiàn)弱引用對象,無論當(dāng)前內(nèi)存空間是否充足,都會將弱引用回收。
虛引用: 如果一個對象具有虛引用,就相當(dāng)于沒有引用,在任何時候都有可能被回收。 使用虛引用的目的就是為了得知對象被GC的時機(jī),所以可以利用虛引用來進(jìn)行銷毀前的一些操作,比如說資源釋放等。
WeakHashMap是基于弱引用的,也就是說只要垃圾回收機(jī)制一開啟,就直接開始了掃蕩,看見了就清除
二、為什么需要 WeakHashMap?
WeakHashMap正是由于使用的是弱引用,因此它的對象可能被隨時回收。
更直觀的說,當(dāng)使用 WeakHashMap 時,即使沒有刪除任何元素,它的尺寸、get方法也可能不一樣。
比如:
(1)調(diào)用兩次 size() 方法返回不同的值;第一次為10,第二次就為8了。
(2)兩次調(diào)用 isEmpty() 方法,第一次返回false,第二次返回true;
(3)兩次調(diào)用 containsKey() 方法,第一次返回true,第二次返回false;
(4)兩次調(diào)用 get() 方法,第一次返回一個value,第二次返回null;
是不是覺得有點惡心,這種飄忽不定的東西好像沒什么用,試想一下,你準(zhǔn)備使用WeakHashMap保存一些數(shù)據(jù),寫著寫著都沒了,那還保存?zhèn)€啥呀。
不過有一種場景,最喜歡這種飄忽不定、一言不合就刪除的東西。
那就是緩存,在緩存場景下,由于內(nèi)存是有限的,不能緩存所有對象,因此就需要一定的刪除機(jī)制,淘汰掉一些對象
現(xiàn)在我們已經(jīng)知道了WeakHashMap是基于弱引用,其對象可能隨時被回收,適用于緩存的場景。
三、WeakHashMap 的例子
public class TestWeakHashMap { public static void main(String[] args) { WeakHashMap<String, String> weakHashMap = new WeakHashMap<>(10); String key0 = new String("str1"); String key1 = new String("str2"); String key2 = new String("str3"); // 存放元素 weakHashMap.put(key0, "data1"); weakHashMap.put(key1, "data2"); weakHashMap.put(key2, "data3"); System.out.printf("weakHashMap: %s\n", weakHashMap); // 是否包含某key System.out.printf("contains key str1 : %s\n", weakHashMap.containsKey(key0)); System.out.printf("contains key str2 : %s\n", weakHashMap.containsKey(key1)); // 移除key weakHashMap.remove(key0); System.out.printf("weakHashMap after remove: %s", weakHashMap); // 這意味著"弱鍵"key1再沒有被其它對象引用,調(diào)用gc時會回收WeakHashMap中與key1對應(yīng)的鍵值對 key1 = null; // 內(nèi)存回收,這里會回收WeakHashMap中與"key0"對應(yīng)的鍵值對 System.gc(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 遍歷WeakHashMap for (Map.Entry<String, String> m : weakHashMap.entrySet()) { System.out.printf("next : %s >>> %s\n", m.getKey(), m.getValue()); } // 打印WeakHashMap的實際大小 System.out.printf("after gc WeakHashMap size: %s\n", weakHashMap.size()); } }
上面的例子展示了WeakHashMap的增刪改查,以及弱鍵的回收,可以看到把Key的引用置為null, gc后,會將該鍵值對回收。
四、WeakHashMap 的使用場景
一般用做緩存,比如Tomcat的源碼里,實現(xiàn)緩存時會用到WeakHashMap, 在緩存系統(tǒng)中,使用WeakHashMap可以避免內(nèi)存泄漏,但是使用WeakHashMap做緩存時要注意,如果只有它的key只有WeakHashMap本身在用,而在WeakHashMap之外沒有對該key的強(qiáng)引用,那么GC時會回收這個key對應(yīng)的entry。
所以WeakHashMap不能用做主緩存,合適的用法應(yīng)該是用它做二級的內(nèi)存緩存,即那么過期緩存數(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的兩個map,對jvm堆區(qū)有所了解的話,可以猜測出tomcat在這里是使用ConcurrentHashMap和WeakHashMap做了分代的緩存。
在put方法里,在插入一個k-v時,先檢查eden緩存的容量是不是超了。
沒有超就直接放入eden緩存,如果超了則鎖定longterm將eden中所有的k-v都放入longterm。
再將eden清空并插入k-v。
在get方法中,也是優(yōu)先從eden中找對應(yīng)的v,如果沒有則進(jìn)入longterm緩存中查找,找到后就加入eden緩存并返回。
經(jīng)過這樣的設(shè)計,相對常用的對象都能在eden緩存中找到,不常用(有可能被銷毀的對象)的則進(jìn)入longterm緩存。
而longterm的key的實際對象沒有其他引用指向它時,gc就會自動回收heap中該弱引用指向的實際對象,弱引用進(jìn)入引用隊列。
longterm調(diào)用expungeStaleEntries()方法,遍歷引用隊列中的弱引用,并清除對應(yīng)的Entry,不會造成內(nèi)存空間的浪費。
五、WeakHashMap 的數(shù)據(jù)結(jié)構(gòu)
1. 類的定義
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> { }
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ù)據(jù)個數(shù)
- threshold : 擴(kuò)容閾值
- loadFactor : 加載因子
- queue : 引用隊列
- modCount : 修改次數(shù)
3. Entry 類
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 類實現(xiàn)了 Map.Entry 接口,繼承弱引用 (WeakReference),屬性有 key,value 和 next 引用。
構(gòu)造器中需要傳入一個引用隊列,方法主要看getKey():
return (K) WeakHashMap.unmaskNull(get());
再來看看 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來選擇是否返回null。
4. 類關(guān)系圖
六、WeakHashMap 的弱鍵回收
上面看完 WeakHashMap 的數(shù)據(jù)結(jié)構(gòu),那么 WeakHashMap 是如何實現(xiàn)弱鍵回收的呢?
其實根據(jù)前面的文章也能猜到,利用Reference和ReferenceQueue。
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。 計算key的hash值,然后根據(jù)hash值查找待插入的位置。
遍歷Entry數(shù)組,看該鍵是否已存在,存在的話則替換舊值,并返回舊值。 不存在則構(gòu)建Entry對象存入數(shù)組。
這個流程和HashMap,HashTable等差不多。
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對象,再判斷該Entry是否存在下一個引用(即hash碰撞),遍歷該單鏈表,比較value值。
3. 弱鍵如何回收?
根據(jù)上面的分析就很容易得出了,WeakHashMap內(nèi)部的數(shù)據(jù)存儲是用Entry[]數(shù)組,即鍵值對數(shù)組。
Entry類繼承于WeakReferece(弱引用),
弱引用的特點再重申下:當(dāng) JVM 進(jìn)行垃圾回收時,無論內(nèi)存是否充足,都會回收被弱引用關(guān)聯(lián)的對象。
在構(gòu)造一個Entry對象的時候,會傳入一個ReferenceQueue,key為弱引用包裹的對象:
public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); }
再來看看WeakHashMap如何清理已經(jīng)被回收的key的,被回收的key會存放在引用隊列中:
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; } } } }
遍歷引用隊列,然后刪除已被回收的鍵值對(從數(shù)組移除,改變單鏈表結(jié)點引用,將value賦值為null),該方法會在WeakHashMap增刪改查、擴(kuò)容的地方調(diào)用。
因為一個key-value存放到WeakHashMap中后,key會被用弱引用包起來存儲,如果這個key在WeakHashMap外部沒有強(qiáng)引用的話,GC時將被回收,然后WeakHashMap根據(jù)引用隊列對已回收的key做清理。
到此這篇關(guān)于Java中WeakHashMap的工作原理詳解的文章就介紹到這了,更多相關(guān)WeakHashMap工作原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
深入探究 spring-boot-starter-parent的作用
這篇文章主要介紹了spring-boot-starter-parent的作用詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,感興趣的小伙伴可以跟著小編一起來學(xué)習(xí)一下2023-05-05使用Java?Executors創(chuàng)建線程池的9種方法
文章主要介紹了?Java?中Executors類創(chuàng)建線程池的?9?種方法,每種方法都詳細(xì)闡述了實現(xiàn)原理、源代碼分析、參數(shù)解釋、實現(xiàn)過程、特性和使用場景,感興趣的小伙伴跟著小編一起來看看吧2024-11-11jdk-logging?log4j?logback日志系統(tǒng)實現(xiàn)機(jī)制原理介紹
這篇文章主要介紹了jdk-logging、log4j、logback日志介紹以及三個日志系統(tǒng)的實現(xiàn)機(jī)制,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03