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