WeakHashMap
1 什么是WeakHashMap?
从名字可以得知主要和Map有关,不过还有一个Weak,我们就更能自然而然的想到这里面还牵扯到一种弱引用结构,
典型使用场景: tomcat两级缓存使用ConcurrentHashMap和WeakHashMap做了分代的缓存。
tomcat的源码里,实现缓存时会用到WeakHashMap因此想要彻底搞懂,我们还需要知道四种引用。
1、四种引用
在jvm中,一个对象如果不再被使用就会被当做垃圾给回收掉,判断一个对象是否是垃圾,通常有两种方法:引用计数法和可达性分析法。不管是哪一种方法判断一个对象是否是垃圾的条件总是一个对象的引用是都没有了。
JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用、软引用、弱引用、虚引用4 种。而我们的WeakHashMap就是基于弱引用。
(1)强引用
如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。比如String str = "hello"这时候str就是一个强引用。
(2)软引用
内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
Integer prime = 1;
SoftReference<Integer> soft = new SoftReference<Integer>(prime);
prime = null;
(3)弱引用
如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。
Integer prime = 1;
WeakReference<Integer> soft = new WeakReference<Integer>(prime);
prime = null;
(4)虚引用
如果一个对象具有虚引用,就相当于没有引用,在任何时候都有可能被回收。使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。
我们的WeakHashMap是基于弱引用的,也就是说只要垃圾回收机制一开启,就直接开始了扫荡,看见了就清除。
这个“弱键”的原理呢?大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:
(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。
实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
(02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。
这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。
和HashMap一样,WeakHashMap是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap
既然有WeakHashMap,那么有WeakHashSet吗? java collections包是没有直接提供WeakHashSet的。
我们可以通过Collections.newSetFromMap(Map<E,Boolean> map)方法可以将任何 Map包装成一个Set。
引用队列(Reference Quene)
一旦弱引用返回null值,那么其指向的对象(即Widget)就变成了垃圾,这个弱引用对象(即weakWidget)也就没有用了。这通常意味着要进行一定方式的清理(cleanup)。例如,WeakHashmap将会移除一些死的(dread)的entry,避免持有过多死的弱引用。
ReferenceQuene能够轻易的追踪这些死掉的弱引用。可以讲ReferenceQuene传入WeakHashmap的构造方法(constructor)中,这样,一旦这个弱引用指向的对象成为垃圾,这个弱引用将加入ReferenceQuene中。
2 为什么需要WeakHashMap
WeakHashMap正是由于使用的是弱引用,因此它的对象可能被随时回收。更直观的说,当使用 WeakHashMap 时,即使没有删除任何元素,它的尺寸、get方法也可能不一样。比如:
(1)调用两次size()方法返回不同的值;第一次为10,第二次就为8了。
(2)两次调用isEmpty()方法,第一次返回false,第二次返回true;
(3)两次调用containsKey()方法,第一次返回true,第二次返回false;
(4)两次调用get()方法,第一次返回一个value,第二次返回null;
是不是觉得有点恶心,这种飘忽不定的东西好像没什么用,试想一下,你准备使用WeakHashMap保存一些数据,写着写着都没了,那还保存个啥呀。
不过有一种场景,最喜欢这种飘忽不定、一言不合就删除的东西。那就是缓存。在缓存场景下,由于内存是有限的,不能缓存所有对象,因此就需要一定的删除机制,淘汰掉一些对象。
现在我们已经知道了WeakHashMap是基于弱引用,其对象可能随时被回收,适用于缓存的场景。下面我们就来看看,WeakHashMap是如何实现这些功能。
3 WeakHashMap工作原理
3.1 WeakHashMap为什么具有弱引用的特点:随时被回收对象
这个问题就比较简单了,我们的目的主要是验证。WeakHashMap是基于弱引用的,肯定就具有了弱引用的性质。我们去他的源码中看一下:
从这里我们可以看到其内部的Entry继承了WeakReference,也就是弱引用,所以就具有了弱引用的特点。不过还要注意一点,那就是ReferenceQueue,他的作用是GC会清理掉对象之后,引用对象会被放到ReferenceQueue中。
新建WeakHashMap,将Entry添加到WeakHashMap中,通过数组table保存Entry。
弱键在没有被强引用引用的前提下,下一次GC将回收该弱键并添加到ReferenceQueue(queue)队列中。
再一次操作WeakHashMap时,会先同步table和queue。table中保存了全部的entry,queue中保存被GC回收的entry;同步它们,就是删除table中被GC回收的键值对。
3.2 WeakHashMap中的Entry被GC后,WeakHashMap是如何将其移除的?
意思是某一个Entry突然被垃圾回收了,这之后WeakHashMap肯定就不能保留这个Entry了,那他是如何将其移除的呢?
WeakHashMap内部有一个expungeStaleEntries函数,在这个函数内部实现移除其内部不用的entry从而达到的自动释放内存的目的。因此我们每次访问WeakHashMap的时候,都会调用这个expungeStaleEntries函数清理一遍。这也就是为什么前两次调用WeakHashMap的size()方法有可能不一样的原因。我们可以看看是如何实现的:
首先GC每次清理掉一个对象之后,引用对象会被放到ReferenceQueue中。然后遍历这个queue进行删除即可。
当然。WeakHashMap的增删改查操作都会直接或者间接的调用expungeStaleEntries()方法,达到及时清除过期entry的目的。
WeakHashMap中的大部分操作都会调用expungeStaleEntries()这个方法,储存被gc回收对象的queue会接收gc发送的回收消息,将queue中的key所对应的value赋值null,帮助完成对Entry的回收工作。
4 WeakHashMap的使用
1、缓存中使用
在文章一开始提到WeakHashMap可以在缓存中使用。下面我们看一下如何使用的:
2、不要使用基础类型作为WeakHashMap的key
缓存的使用案例太多了,这里举一个WeakHashMap使用不规范的例子。
objectMap.put方法执行的时候i会被封装为Integer类型的,Integer保留了-128到127的缓存。但是对于int来说范围大很多,因此那些Key <= 127的Entry将不会进行自动回收,但是那些大于127的将会被回收,因此最后的尺寸总是会稳定在128左右。
5 WeakHashMap的关键实现
private static class Entry<K,V> extends WeakReference 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的构造函数中可以看出:Entry通过传入key和queue调用了父类WeakReference的构造函数,那么key就成为了这个弱引用所引用的对象,并把这个弱引用注册到了引用队列上。
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
因为存储在Entry中的key只具有弱引用,所以并不能阻止垃圾回收线程对它进行回收,当发生垃圾回收时,Entry中的key被回收,java虚拟机就会把这个Entry添加到与之关联的queue中去。
通过上面的分析,存储在WeakHashMap中的key随时都会面临被回收的风险,因此每次查询WeakHashMap时,都要确认当前WeakHashMap是否已经有key被回收了。当key被回收时,引用这个key的Entry对象就会被添加到引用队列中去,所以只要查询引用队列是否有Entry对象,就可以确认是否有key被回收了。WeakHashMap通过调用expungeStaleEntries方法来清除已经被回收的key所关联的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在调用put和get方法之前,都会调用expungeStaleEntries方法来清除已经被回收的key所关联的Entry对象。因为Entry是弱引用,即使引用着key对象,但是依然不能阻止垃圾回收线程对key对象的回收。
如果存放在WeakHashMap中的key都存在强引用,那么WeakHashMap就会退化成HashMap。如果在系统中希望通过WeakHashMap自动清除数据,请尽量不要在系统的其他地方强引用WeakHashMap的key,否则,这些key就不会被回收,WeakHashMap也就无法正常释放它们所占用的表项。
6 案例应用
如果在一个普通的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关联的value对象,要尽可能的多调用下put/size/get等操作,因为这些方法会调用expungeStaleEntries方法,expungeStaleEntries方法是关键,而如果不操作WeakHashMap,以企图WeakHashMap“自动”释放内存是不可取的,这里的“自动”是指譬如:map.put(obj, new byte[10M]);之后obj=null了,之后再也没调用过map的任何方法,那么new出来的10M空间是不会释放的。
注意
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
运行结果:
[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结束====================
[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结束====================
[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结束====================
[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结束====================
[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)
7 总结
1、在WeakHashMap中,put的key为null时,放入的是NULL_KEY,即:private static final Object NULL_KEY = new Object(),是一个静态常量。
2、在WeakHashMap中,由于传给WeakReference的只有key和queue,即gc只回收里面的KEY,而不会动value,value的清除则是在expungeStaleEntries这个私有方法进行的。
3、而static的就不在gc之列,所以key也就不会被gc,所以它的大值value,也就不会被设为null,不会被回收。
4、通过调用remove方法,最终table[k]设为null,此时大对象游离所以被回收。
只有通过remove方法才能删除null键所关联的value,建议在使用WeakHashMap的时候尽量避免使用null作为键。
在如今的并发泛滥的大环境下,大家应该都用过缓存,缓存都是放在内存中的,而内存几乎是计算机中最宝贵也是最稀缺的资源,所以需要谨慎的使用,不然很容易就出现 OOM。缓存的主要作用是为了更快的处理业务、降低服务器的压力,那么就要保证缓存命中率,这里假设整个缓存是一个 key-value 结构的(以键值对缓存为例),HashMap 作为强引用对象在没有主动将 key 删除时是不会被 JVM 回收的,这样 HashMap 中的对象就会越积越多直到 OOM 错误;那么如何做到既让缓存的命中率高又不占用那么多的内存,这里就可以采用 WeakHashMap,当然不会有 HashMap 100% 的命中率(假设内存足够),但是在保证程序正常的前提下更好的实现了缓存这套解决方案。
WeakHashMap 内部是通过弱引用来管理 Entry 的,弱引用的特性对应到 WeakHashMap 上意味着什么呢?将一对 key-value 放入到 WeakHashMap 里并不能避免该 key 值被 GC 回收(除非在 WeakHashMap 之外还有对该 key 的强引用),故内存问题也可以解决。
public static <E> Set<E> newSetFromMap(Map<E, Boolean> map) {
return new SetFromMap<>(map);
}
/**
* @serial include
*/
private static class SetFromMap<E> extends AbstractSet<E>
implements Set<E>, Serializable
{
private final Map<E, Boolean> m; // The backing map
private transient Set<E> s; // Its keySet
SetFromMap(Map<E, Boolean> map) {
if (!map.isEmpty())
throw new IllegalArgumentException("Map is non-empty");
m = map;
s = map.keySet();
}
public void clear() { m.clear(); }
public int size() { return m.size(); }
public boolean isEmpty() { return m.isEmpty(); }
public boolean contains(Object o) { return m.containsKey(o); }
public boolean remove(Object o) { return m.remove(o) != null; }
public boolean add(E e) { return m.put(e, Boolean.TRUE) == null; }
public Iterator<E> iterator() { return s.iterator(); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
public String toString() { return s.toString(); }
public int hashCode() { return s.hashCode(); }
public boolean equals(Object o) { return o == this || s.equals(o); }
public boolean containsAll(Collection<?> c) {return s.containsAll(c);}
public boolean removeAll(Collection<?> c) {return s.removeAll(c);}
public boolean retainAll(Collection<?> c) {return s.retainAll(c);}