歡迎支持筆者新作:《深入理解Kafka:核心設計與實踐原理》和《RabbitMQ實戰指南》,同時歡迎關注筆者的微信公眾號:朱小廝的博客。
歡迎跳轉到本文的原文鏈接:https://honeypps.com/java/java-collection-weekhashmap/
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;elseprev.next = next;// Must not null out e.next;// stale entries may be in use by a HashIteratore.value = null; // Help GCsize--;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 spaceat 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 spaceat 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=============
HeapPSYoungGen 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程序性能優化——讓你的Java程序更快、更穩定》葛一鳴 等編著。
歡迎跳轉到本文的原文鏈接:https://honeypps.com/java/java-collection-weekhashmap/
歡迎支持筆者新作:《深入理解Kafka:核心設計與實踐原理》和《RabbitMQ實戰指南》,同時歡迎關注筆者的微信公眾號:朱小廝的博客。
總結
以上是生活随笔 為你收集整理的Java集合框架:WeakHashMap 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。