WeakHashMap的垃圾回收原理詳解
WeakHashMap 介紹
WeakHashMap 與 HashMap 的用法基本類似。與 HashMap 的區(qū)別在于,HashMap 的key 保留了對實際對象的強引用個,這意味著只要該HashMap對象不被銷毀,該HashMap的所有key所引用的對象就不會被垃圾回收,HashMap也不會自動刪除這些key所對應的key-value 對;
但WeakHashMap的key 只保留了對實際對象的弱引用,這意味著如果WeakHashMap對象的key所引用的對象沒有被其他強引用變量所引用個,則這些key所引用的對象可能被垃圾回收,WeakHashMap也可能自動刪除這些key所對應的key-value對。
WeakHashMap的數(shù)據(jù)結構
類的定義

public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V> {
}WeakHashMap 因為GC的時候會把沒有強引用的key回收掉,所以它里面的元素不會太多。
因此,WeakHashMap 的存儲結構只有 數(shù)組 + 鏈表
變量和常量
// 默認初始容量為16
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量為2的30次方
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默認裝載因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 桶
Entry<K,V>[] table;
//元素個數(shù)
private int size;
// 擴容門檻,等于capacity * loadFactor
private int threshold;
// 裝載因子
private final float loadFactor;
/**
* 引用隊列,當弱鍵失效的時候會把Entry添加到這個隊列中
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 修改次數(shù)
int modCount;Entry 內(nèi)部類
Entry 內(nèi)部類并沒有key屬性,因為key屬性存儲在 Reference 類中
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
// 調(diào)用Reference的構造方法初始化key和引用隊列
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
}
public class WeakReference<T> extends Reference<T> {
// 調(diào)用Reference的構造方法初始化 key 和 引用隊列
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
public abstract class Reference<T> {
// 實際存儲key的地方
private T referent;
// 引用隊列
volatile ReferenceQueue<? super T> queue;
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}從Entry的構造方法我們知道,key和queue最終會傳到到Reference的構造方法中,這里的key就是Reference的referent屬性,它會被gc特殊對待,即當沒有強引用存在時,當下一次gc的時候會被清除。
構造方法
public WeakHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Initial Capacity: "+
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load factor: "+
loadFactor);
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
table = newTable(capacity);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
}
public WeakHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public WeakHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public WeakHashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR);
putAll(m);
}構造方法和 HashMap基本類似,初始容量為大于等于傳入容量的2的n次方,擴容的門檻 threshold 等于 capacity * loadFactor 。
put(K key, V value)
public V put(K key, V value) {
// 如果key為空,用空對象代替
Object k = maskNull(key);
// 計算key的hash值
int h = hash(k);
// 獲取桶
Entry<K,V>[] tab = getTable();
// 計算元素在哪個桶中,h & (length-1)
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);
// 如果插入元素后數(shù)量達到了擴容門檻就把桶的數(shù)量擴容為2倍大小
if (++size >= threshold)
resize(tab.length * 2);
return null;
}(1)計算hash;
與HashMap不同的是 ,key為null 時,返回的hash時0
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}而 WeakHashMap 用空對象來計算
private static final Object NULL_KEY = new Object();
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}另外,HashMap 計算hash 只用了依次異或,而這里使用了四次
final int hash(Object k) {
int h = k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}(2) 計算再哪個桶
(3) 遍歷桶對應的鏈表
(4) 如果能找到元素,則用新值代替舊值
(5) 如果沒有找到就在鏈表的頭部插入新元素
(6) 如果元素數(shù)量達到了擴容門檻,就把容量擴大到原來容量的2倍;
resize(int newCapacity)
void resize(int newCapacity) {
// 獲取舊桶,getTable()的時候會剔除失效的Entry
Entry<K,V>[] oldTable = getTable();
// 舊容量
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新桶
Entry<K,V>[] newTable = newTable(newCapacity);
// 把元素從舊桶轉移到新桶
transfer(oldTable, newTable);
table = newTable;
// 如果元素個數(shù)大于擴容門檻的一半,則使用新桶和新容量,并計算新的擴容門檻
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
// 否則把元素再轉移回舊桶,還是使用舊桶
// 因為在transfer的時候會清除失效的Entry,所以元素個數(shù)可能沒有那么大了,就不需要擴容了
expungeStaleEntries();
transfer(newTable, oldTable);
table = oldTable;
}
}
private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
// 遍歷舊桶
for (int j = 0; j < src.length; ++j) {
Entry<K,V> e = src[j];
src[j] = null;
while (e != null) {
Entry<K,V> next = e.next;
Object key = e.get();
// 如果key等于了null就清除,說明key被gc清理掉了,則把整個Entry清除
if (key == null) {
e.next = null; // Help GC
e.value = null; // " "
size--;
} else {
// 否則就計算在新桶中的位置并把這個元素放在新桶對應鏈表的頭部
int i = indexFor(e.hash, dest.length);
e.next = dest[i];
dest[i] = e;
}
e = next;
}
}
}(1)判斷舊容量是否達到最大容量;
(2)新建新桶并把元素全部轉移到新桶中;
(3)如果轉移后元素個數(shù)不到擴容門檻的一半,則把元素再轉移回舊桶,繼續(xù)使用舊桶,說明不需要擴 容;
(4)否則使用新桶,并計算新的擴容門檻;
(5)轉移元素的過程中會把key為null的元素清除掉,所以size會變??;
垃圾回收原理
WeakHashMap 通過將一些沒有被引用的鍵的值賦值為null, 這樣就會告訴GC去回收這些存儲的值。
我們看下面例子:
public class WeakHashMapTest {
public static void main(String[] args) {
House seller1 = new House("1號賣家房源.");
SellerInfo sellerInfo1 = new SellerInfo();
House seller2 = new House("2號賣家房源");
SellerInfo sellerInfo2 = new SellerInfo();
WeakHashMap<House,SellerInfo> weakHashMap = new WeakHashMap<>();
//如果換成 HashMap ,則Key是對House對象的強引用
weakHashMap.put(seller1,sellerInfo1);
weakHashMap.put(seller2,sellerInfo2);
System.out.println("weakHashMap before null,size="+weakHashMap.size());
seller1 = null;
System.gc();
System.runFinalization();
//如果換成 HashMap ,size 依然等于2
System.out.println("weakHashMap after null, size = "+weakHashMap.size());
System.out.println(weakHashMap);
}
}
class SellerInfo{}最終的結果是size = 1,為什么為1呢? 因為 seller1 為null, 從而引起GC,那么 為什么我們把 null 作為鍵存進去,為什么不會導致被回收呢?
那么我們看 put 方法的源碼:
public V put(K key, V value) {
K k = (K) maskNull(key);// 重點看這里
int h = HashMap.hash(k.hashCode());
Entry[] 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,V>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);
return null;
}我們重點看一些 這行代碼 K k = (K) maskNull(key);
private static final Object NULL_KEY = new Object();
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}如果key為null的話,返回 的是NULL_KEY 這個靜態(tài)值,這個靜態(tài)值就是 Object ,所以WeakHashMap 在存儲null為鍵的時候,其實存儲的是其本身的靜態(tài)成員變量 Object,也就是存儲不是null。
那WeakHashMap 是如何跟WeakReference 關聯(lián)起來的呢?
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
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;
}
}WeakHashMap的Entry是繼承WeakReference,這樣一來,整個Entry就是一個WeakReference,再來看看Entry的構造方法,調(diào)用了super(key, queue),也就是調(diào)用了這個構造方法
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}有兩個參數(shù),一個key,一個是queue, 這個key就是WeakHashMap 中存儲的key的值,這個queue 是WeakHashMap 中創(chuàng)建的 ReferenceQueue 。 那么 ReferenceQueue 有什么用呢?
當GC某個對象時,如果有此對象上還有弱引用與其關聯(lián),會將WeakReference對象與Reference 類的pending 引用關聯(lián)起來,然后由 Reference Handler線程將該插入ReferenceQueue隊列。
也就是說Entry中的key被GC時,會你那個Entry 放入到 ReferenceQueue中,WeakHashMap就能通過ReferenceQueue中的Entry了解到哪些key已經(jīng)被GC,或者即將馬上被GC,起到了通知的作用。
那么什么時候來判斷要講沒有被引用的key標記為null的呢?
在WeakHashMap的put(),get(),remove()等等方法中都調(diào)用了一個getTable()方法,而這個getTable()方法的源碼如下:
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}其實都是調(diào)用 expungeStaleEntries() 方法,我們看其源碼:
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;
}
}
}
}上面代碼中的queue 就是定義的成員變量
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
可以看到每調(diào)用一次expungeStaleEntries()方法,就會在引用隊列中尋找是否有將要被清除的key對象,如果有則在table中找到其值,并將value設置為null,next指針也設置為null,讓GC去回收這些資源。
總結
(1)WeakHashMap使用(數(shù)組 + 鏈表)存儲結構;
(2)WeakHashMap中的key是弱引用,gc的時候會被清除;
(3)每次對map的操作都會剔除失效key對應的Entry;
(4)使用String作為key時,一定要使用new String()這樣的方式聲明key,才會失效,其它的基本類型的包裝類型是一樣的;
(5)WeakHashMap常用來作為緩存使用;
到此這篇關于WeakHashMap的垃圾回收原理詳解的文章就介紹到這了,更多相關WeakHashMap垃圾回收內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Nacos客戶端配置中心緩存動態(tài)更新實現(xiàn)源碼
這篇文章主要為大家介紹了Nacos客戶端配置中心緩存動態(tài)更新實現(xiàn)源碼,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-03-03
Spring Cloud微服務架構的構建:分布式配置中心(加密解密功能)
這篇文章主要給大家介紹了關于Spring Cloud微服務架構的構建:分布式配置中心(加密解密)的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用具有一定的參考學習價值,需要的朋友可以參考下2018-05-05
SpringBoot項目集成Swagger和swagger-bootstrap-ui及常用注解解讀
這篇文章主要介紹了SpringBoot項目集成Swagger和swagger-bootstrap-ui及常用注解解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03

