深入理解Java中的WeakHashMap
一、什么是WeakHashMap?
從名字可以得知主要和Map有關(guān),不過還有一個Weak,我們就更能自然而然的想到這里面還牽扯到一種弱引用結(jié)構(gòu),因此想要徹底搞懂,我們還需要知道四種引用。
如果你已經(jīng)知道了,可以跳過。
四種引用
在jvm中,一個對象如果不再被使用就會被當(dāng)做垃圾給回收掉,判斷一個對象是否是垃圾,通常有兩種方法:引用計數(shù)法和可達性分析法。
不管是哪一種方法判斷一個對象是否是垃圾的條件總是一個對象的引用是都沒有了。
JDK.1.2 之后,Java 對引用的概念進行了擴充,將引用分為了:強引用、軟引用、弱引用、虛引用4 種。
而我們的WeakHashMap就是基于弱引用。
(1)強引用
如果一個對象具有強引用,它就不會被垃圾回收器回收。即使當(dāng)前內(nèi)存空間不足,JVM也不會回收它,而是拋出 OutOfMemoryError 錯誤,使程序異常終止。
比如String str = "hello"這時候str就是一個強引用。
(2)軟引用
內(nèi)存足夠的時候,軟引用對象不會被回收,只有在內(nèi)存不足時,系統(tǒng)則會回收軟引用對象,如果回收了軟引用對象之后仍然沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。
(3)弱引用
如果一個對象具有弱引用,在垃圾回收時候,一旦發(fā)現(xiàn)弱引用對象,無論當(dāng)前內(nèi)存空間是否充足,都會將弱引用回收。
(4)虛引用
如果一個對象具有虛引用,就相當(dāng)于沒有引用,在任何時候都有可能被回收。使用虛引用的目的就是為了得知對象被GC的時機,所以可以利用虛引用來進行銷毀前的一些操作,比如說資源釋放等。
我們的WeakHashMap是基于弱引用的,也就是說只要垃圾回收機制一開啟,就直接開始了掃蕩,看見了就清除。
二、為什么需要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)存是有限的,不能緩存所有對象,因此就需要一定的刪除機制,淘汰掉一些對象。
現(xiàn)在我們已經(jīng)知道了WeakHashMap是基于弱引用,其對象可能隨時被回收,適用于緩存的場景。下面我們就來看看,WeakHashMap是如何實現(xiàn)這些功能。
三、WeakHashMap工作原理
1、WeakHashMap為什么具有弱引用的特點:隨時被回收對象
這個問題就比較簡單了,我們的目的主要是驗證。WeakHashMap是基于弱引用的,肯定就具有了弱引用的性質(zhì)。我們?nèi)ニ脑创a中看一下:
從這里我們可以看到其內(nèi)部的Entry繼承了WeakReference,也就是弱引用,所以就具有了弱引用的特點。不過還要注意一點,那就是ReferenceQueue,他的作用是GC會清理掉對象之后,引用對象會被放到ReferenceQueue中。
2、WeakHashMap中的Entry被GC后,WeakHashMap是如何將其移除的?
意思是某一個Entry突然被垃圾回收了,這之后WeakHashMap肯定就不能保留這個Entry了,那他是如何將其移除的呢?
WeakHashMap內(nèi)部有一個expungeStaleEntries函數(shù),在這個函數(shù)內(nèi)部實現(xiàn)移除其內(nèi)部不用的entry從而達到的自動釋放內(nèi)存的目的。
因此我們每次訪問WeakHashMap的時候,都會調(diào)用這個expungeStaleEntries函數(shù)清理一遍。這也就是為什么前兩次調(diào)用WeakHashMap的size()方法有可能不一樣的原因。我們可以看看是如何實現(xiàn)的:
首先GC每次清理掉一個對象之后,引用對象會被放到ReferenceQueue中。
然后遍歷這個queue進行刪除即可。
當(dāng)然。WeakHashMap的增刪改查操作都會直接或者間接的調(diào)用expungeStaleEntries()方法,達到及時清除過期entry的目的。
四、WeakHashMap的關(guān)鍵實現(xiàn)
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
Entry繼承自WeakReference(弱引用),那么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的構(gòu)造函數(shù)中可以看出:Entry通過傳入key和queue調(diào)用了父類WeakReference的構(gòu)造函數(shù),那么key就成為了這個弱引用所引用的對象,并把這個弱引用注冊到了引用隊列上。
如果一個對象只具有弱引用,那就類似于可有可無的生活用品。只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會回收它的內(nèi)存。不過,由于垃圾回收器是一個優(yōu)先級很低的線程, 因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象。 弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關(guān)聯(lián)的引用隊列中。
因為存儲在Entry中的key只具有弱引用,所以并不能阻止垃圾回收線程對它進行回收,當(dāng)發(fā)生垃圾回收時,Entry中的key被回收,java虛擬機就會把這個Entry添加到與之關(guān)聯(lián)的queue中去。
通過上面的分析,存儲在WeakHashMap中的key隨時都會面臨被回收的風(fēng)險,因此每次查詢WeakHashMap時,都要確認當(dāng)前WeakHashMap是否已經(jīng)有key被回收了。當(dāng)key被回收時,引用這個key的Entry對象就會被添加到引用隊列中去,所以只要查詢引用隊列是否有Entry對象,就可以確認是否有key被回收了。WeakHashMap通過調(diào)用 expungeStaleEntries
方法來清除已經(jīng)被回收的key所關(guān)聯(lián)的Entry對象。
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; } } } }
WeakHashMap在調(diào)用 put
和 get
方法之前,都會調(diào)用 expungeStaleEntries
方法來清除已經(jīng)被回收的key所關(guān)聯(lián)的Entry對象。
因為Entry是弱引用,即使引用著key對象,但是依然不能阻止垃圾回收線程對key對象的回收。
如果存放在WeakHashMap中的key都存在強引用,那么WeakHashMap就會退化成HashMap。如果在系統(tǒng)中希望通過WeakHashMap自動清除數(shù)據(jù),請盡量不要在系統(tǒng)的其他地方強引用WeakHashMap的key,否則,這些key就不會被回收,WeakHashMap也就無法正常釋放它們所占用的表項。
五、案例應(yīng)用
如果在一個普通的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]); }
運行參數(shù):-Xmx5M 運行結(jié)果:
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]); }
運行結(jié)果:(無任何報錯)
這兩段代碼比較可以看到WeakHashMap的功效,如果在系統(tǒng)中需要一張很大的Map表,Map中的表項作為緩存使用,這也意味著即使沒能從該Map中取得相應(yīng)的數(shù)據(jù),系統(tǒng)也可以通過候選方案獲取這些數(shù)據(jù)。雖然這樣會消耗更多的時間,但是不影響系統(tǒng)的正常運行。
在這種場景下,使用WeakHashMap是最合適的。因為WeakHashMap會在系統(tǒng)內(nèi)存范圍內(nèi),保存所有表項,而一旦內(nèi)存不夠,在GC時,沒有被引用的表項又會很快被清除掉,從而避免系統(tǒng)內(nèi)存溢出。
我們這里稍微改變一下上面的代碼(加了一個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]); }
運行結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at collections.WeakHashMapTest.main(WeakHashMapTest.java:43)
如果存放在WeakHashMap中的key都存在強引用,那么WeakHashMap就會退化成HashMap。
如果在系統(tǒng)中希望通過WeakHashMap自動清除數(shù)據(jù),請盡量不要在系統(tǒng)的其他地方強引用WeakHashMap的key,否則,這些key就不會被回收,WeakHashMap也就無法正常釋放它們所占用的表項。
要想WeakHashMap能夠釋放掉被回收的key關(guān)聯(lián)的value對象,要盡可能的多調(diào)用下put/size/get等操作,因為這些方法會調(diào)用expungeStaleEntries方法,expungeStaleEntries方法是關(guān)鍵,而如果不操作WeakHashMap,以企圖WeakHashMap“自動”釋放內(nèi)存是不可取的,這里的“自動”是指譬如:map.put(obj, new byte[10M]);之后obj=null了,之后再也沒調(diào)用過map的任何方法,那么new出來的10M空間是不會釋放的。
注意
WeakHashMap的key可以為null,那么當(dāng)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結(jié)束===================="); }
運行參數(shù):-Xmx5M -XX:+PrintGCDetails
運行結(jié)果:
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結(jié)束====================
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結(jié)束====================
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結(jié)束====================
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結(jié)束====================
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結(jié)束====================
(一直循環(huán)下去)
可以看到在 map.put(null, new byte[5*1024*928]);
之后,相應(yīng)的內(nèi)存一直沒有得到釋放。
通過顯式的調(diào)用 map.remove(null)
可以將內(nèi)存釋放掉,如下代碼所示:
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=============");
運行參數(shù):-Xmx5M -XX:+PrintGCDetails
運行結(jié)果:
[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)
分析:
1、在WeakHashMap中,put的key為null時,放入的是NULL_KEY,即:private static final Object NULL_KEY = new Object(),是一個靜態(tài)常量。
2、在WeakHashMap中,由于傳給WeakReference的只有key和queue,即gc只回收里面的KEY,而不會動value,value的清除則是在expungeStaleEntries這個私有方法進行的。
3、而static的就不在gc之列,所以key也就不會被gc,所以它的大值value,也就不會被設(shè)為null,不會被回收。
4、通過調(diào)用remove方法,最終table[k]設(shè)為null,此時大對象游離所以被回收。
只有通過remove方法才能刪除null鍵所關(guān)聯(lián)的value,建議在使用WeakHashMap的時候盡量避免使用null作為鍵。
到此這篇關(guān)于深入理解Java中的WeakHashMap的文章就介紹到這了,更多相關(guān)Java中的WeakHashMap內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?Boot?4.0對于Java開發(fā)的影響和前景
探索Spring?Boot?4.0如何徹底革新Java開發(fā),提升效率并開拓未來可能性!別錯過這篇緊湊的指南,它帶你領(lǐng)略Spring?Boot的強大魅力和潛力,準(zhǔn)備好了嗎?2024-02-02Java Date類常用示例_動力節(jié)點Java學(xué)院整理
在JDK1.0中,Date類是唯一的一個代表時間的類,但是由于Date類不便于實現(xiàn)國際化,所以從JDK1.1版本開始,推薦使用Calendar類進行時間和日期處理。這里簡單介紹一下Date類的使用,需要的朋友可以參考下2017-05-05Java數(shù)據(jù)結(jié)構(gòu)之對象的比較
比較對象是面向?qū)ο缶幊陶Z言的一個基本特征,下面這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)結(jié)構(gòu)之對象的比較,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-02-02