Java集合框架:WeakHashMap
WeakHashMap定義
package java.util;
import java.lang.ref.WeakReference;
import java.lang.ref.ReferenceQueue;
public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V> {
}
WeakHashMap實現了Map介面,是HashMap的一種實現,它比HashMap多了一個引用佇列:
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
博主認真比對過WeakHashMap和JDK7 HashMap的原始碼,發現WeakHashMap中方法的實現方式基本和JDK7 HashMap的一樣,注意“基本”兩個字,除了沒有實現Cloneable和Serializable這兩個標記介面,最大的區別在於在於expungeStaleEntries()這個方法,這個是整個WeakHashMap的精髓,我們稍後進行闡述。
它使用弱引用作為內部資料的儲存方案。WeakHashMap是弱引用的一種典型應用,它可以為簡單的快取表解決方案。
WeakHashMap使用
我們舉一個簡單的例子來說明一下WeakHashMap的使用:
Map<String,Integer> map = new WeakHashMap<>(); map.put("s1", 1); map.put("s2", 2); map.put("s3", 3); map.put("s4", 4); map.put("s5", 5); map.put(null, 9); map.put("s6", 6); map.put("s7", 7); map.put("s8", 8); map.put(null, 11); for(Map.Entry<String,Integer> entry:map.entrySet()) { System.out.println(entry.getKey()+":"+entry.getValue()); } System.out.println(map);
執行結果:
s4:4
s3:3
s6:6
null:11
s5:5
s8:8
s7:7
s1:1
s2:2
{s4=4, s3=3, s6=6, null=11, s5=5, s8=8, s7=7, s1=1, s2=2}
WeakHashMap和HashMap一樣key和value的值都可以為null,並且也是無序的。但是HashMap的null是存在table[0]中的,這是固定的,並且null的hash為0,而在WeakHashMap中的null卻沒有存入table[0]中。
這是因為WeakHashMap對null值進行了包裝:
private static final Object NULL_KEY = new Object(); private static Object maskNull(Object key) { return (key == null) ? NULL_KEY : key; } static Object unmaskNull(Object key) { return (key == NULL_KEY) ? null : key; }
當對map進行put和get操作的時候,將null值標記為NULL_KEY,然後對NULL_KEY即對new Object()進行與其他物件一視同仁的hash,這樣就使得null和其他非null的值毫無區別。
JDK關鍵原始碼分析
首先看一下Entry<K,V>這個靜態內部類:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
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;
}
//其餘程式碼略
}
可以看到Entry繼承擴充套件了WeakReference類(有關Java的引用型別可以參考《Java引用型別》)。並在其建構函式中,構造了key的弱引用。
此外,在WeakHashMap的各項操作中,比如get()、put()、size()都間接或者直接呼叫了expungeStaleEntries()方法,以清理持有弱引用的key的表象。
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;
}
}
}
}
可以看到每呼叫一次expungeStaleEntries()方法,就會在引用佇列中尋找是否有被清楚的key物件,如果有則在table中找到其值,並將value設定為null,next指標也設定為null,讓GC去回收這些資源。
案例應用
如果在一個普通的HashMap中儲存一些比較大的值如下:
Map<Integer,Object> map = new HashMap<>();
for(int i=0;i<10000;i++)
{
Integer ii = new Integer(i);
map.put(ii, new byte[i]);
}
執行引數:-Xmx5M
執行結果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at collections.WeakHashMapTest.main(WeakHashMapTest.java:39)
同樣我們將HashMap換成WeakHashMap其餘都不變:
Map<Integer,Object> map = new WeakHashMap<>();
for(int i=0;i<10000;i++)
{
Integer ii = new Integer(i);
map.put(ii, new byte[i]);
}
執行結果:(無任何報錯)
這兩段程式碼比較可以看到WeakHashMap的功效,如果在系統中需要一張很大的Map表,Map中的表項作為快取只用,這也意味著即使沒能從該Map中取得相應的資料,系統也可以通過候選方案獲取這些資料。雖然這樣會消耗更多的時間,但是不影響系統的正常執行。
在這種場景下,使用WeakHashMap是最合適的。因為WeakHashMap會在系統記憶體範圍內,儲存所有表項,而一旦記憶體不夠,在GC時,沒有被引用的表項又會很快被清除掉,從而避免系統記憶體溢位。
我們這裡稍微改變一下上面的程式碼(加了一個List):
Map<Integer,Object> map = new WeakHashMap<>();
List<Integer> list = new ArrayList<>();
for(int i=0;i<10000;i++)
{
Integer ii = new Integer(i);
list.add(ii);
map.put(ii, new byte[i]);
}
執行結果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at collections.WeakHashMapTest.main(WeakHashMapTest.java:43)
如果存放在WeakHashMap中的key都存在強引用,那麼WeakHashMap就會退化成HashMap。如果在系統中希望通過WeakHashMap自動清楚資料,請儘量不要在系統的其他地方強引用WeakHashMap的key,否則,這些key就不會被回收,WeakHashMap也就無法正常釋放它們所佔用的表項。
博主如是說:要想WeakHashMap能夠釋放掉key被GC的value的物件,儘可能的多呼叫下put/size/get等操作,因為這些方法會呼叫expungeStaleEntries方法,expungeStaleEntries方法是關鍵,而如果不操作WeakHashMap,以企圖WeakHashMap“自動”釋放記憶體是不可取的,這裡的“自動”是指譬如map.put(obj,new byte[10M]);之後obj=null了,之後再也沒掉用過map的任何方法,那麼new出來的10M空間是不會釋放的。
疑問
樓主對於WeakHashMap一直有一個疑問,是這樣的:
我們知道WeakHashMap的key可以為null,那麼當put一個key為null,value為一個很大物件的時候,這個很大的物件怎麼採用WeakHashMap的自帶的功能自動釋放呢?
程式碼如下:
Map<Object,Object> map = new WeakHashMap<>();
map.put(null,new byte[5*1024*928]);
int i = 1;
while(true)
{
System.out.println();
TimeUnit.SECONDS.sleep(2);
System.out.println(map.size());
System.gc();
System.out.println("==================第"+i+++"次GC結束====================");
}
執行引數:-Xmx5M -XX:+PrintGCDetails
執行結果:
1
[GC [PSYoungGen: 680K->504K(2560K)] 5320K->5240K(7680K), 0.0035741 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 504K->403K(2560K)] [ParOldGen: 4736K->4719K(5120K)] 5240K->5123K(7680K) [PSPermGen: 2518K->2517K(21504K)], 0.0254473 secs] [Times: user=0.06 sys=0.00, real=0.03 secs]
==================第1次GC結束====================
1
[Full GC [PSYoungGen: 526K->0K(2560K)] [ParOldGen: 4719K->5112K(5120K)] 5246K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0172785 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
==================第2次GC結束====================
1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0178421 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
==================第3次GC結束====================
1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0164874 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
==================第4次GC結束====================
1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0191096 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
==================第5次GC結束====================
(一直迴圈下去)
可以看到在map.put(null,new byte[51024928]);之後,相應的記憶體一直沒有得到釋放。
通過顯式的呼叫map.remove(null)可以將記憶體釋放掉(如下程式碼所示)。
Map<Integer,Object> map = new WeakHashMap<>();
System.gc();
System.out.println("===========gc:1=============");
map.put(null,new byte[4*1024*1024]);
TimeUnit.SECONDS.sleep(5);
System.gc();
System.out.println("===========gc:2=============");
TimeUnit.SECONDS.sleep(5);
System.gc();
System.out.println("===========gc:3=============");
map.remove(null);
TimeUnit.SECONDS.sleep(5);
System.gc();
System.out.println("===========gc:4=============");
執行引數:-Xmx5M -XX:+PrintGCDetails
執行結果:
[GC [PSYoungGen: 720K->504K(2560K)] 720K->544K(6144K), 0.0023652 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 40K->480K(3584K)] 544K->480K(6144K) [PSPermGen: 2486K->2485K(21504K)], 0.0198023 secs] [Times: user=0.11 sys=0.00, real=0.02 secs]
===========gc:1=============
[GC [PSYoungGen: 123K->32K(2560K)] 4699K->4608K(7680K), 0.0026722 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4576K->4578K(5120K)] 4608K->4578K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0145734 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
===========gc:2=============
[GC [PSYoungGen: 40K->32K(2560K)] 4619K->4610K(7680K), 0.0013068 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4578K->4568K(5120K)] 4610K->4568K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0189642 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
===========gc:3=============
[GC [PSYoungGen: 40K->32K(2560K)] 4609K->4600K(7680K), 0.0011742 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4568K->472K(5120K)] 4600K->472K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0175907 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
===========gc:4=============
Heap
PSYoungGen total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14820,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 5120K, used 472K [0x00000000ff800000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 5120K, 9% used [0x00000000ff800000,0x00000000ff876128,0x00000000ffd00000)
PSPermGen total 21504K, used 2526K [0x00000000fa600000, 0x00000000fbb00000, 0x00000000ff800000)
object space 21504K, 11% used [0x00000000fa600000,0x00000000fa8778f8,0x00000000fbb00000)
如果真是隻有通過remove的方式去刪除null的鍵所指向的value的話,博主建議在使用WeakHashMap的時候儘量避免使用null作為鍵。如果有大神可以解答一下這個問題,請在下方留言。
參考資料:
- 《Java程式效能優化——讓你的Java程式更快、更穩定》葛一鳴 等編著。
歡迎支援《RabbitMQ實戰指南》以及關注微信公眾號:朱小廝的部落格。