WeakHashMap和HashMap的区别
前面对HashMap的源码和WeakHashMap的源码分别进行了分析。在WeakHashMap源码分析博文中有对与HashMap区别的比较,但是不够具体系统。加上本人看了一些相关的博文,发现了一些好的例子来说明这两者的区别,因此,就有了这篇博文。
WeakHashMap和HashMap一样,WeakHashMap也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以为null。不过WeakHashMap的键是“弱键”(注:源码中Entry中的定义是这样的:private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
,即Entry实现了WeakReference类),当WeakHashMap某个键不再正常使用时,会被从WeakHashMap自动删除。更精确的说,对于一个给定的键,其映射的存在并不能阻止垃圾回收器对该键的丢弃,这就使该键称为被终止的,被终止,然后被回收,这样,这就可以认为该键值对应该被WeakHashMap删除。因此,WeakHashMap使用了弱引用作为内部数据的存储方案,,WeakHashMap可以作为简单缓存表的解决方案,当系统内存不足时,垃圾收集器会自动的清除没有在任何其他地方被引用的键值对。如果需要用一张很大的Map作为缓存表时,那么可以考虑使用WeakHashMap。
从源码的角度,我们来分析下上面这段话是如何来工作的??
在WeakHashMap实现中,借用了ReferenceQueue这个“监听器”来保存被GC回收的”弱键”,然后在每次使用WeakHashMap时,就在WeakHashMap中删除ReferenceQueue中保存的键值对。即WeakHashMap的实现是通过借用
ReferenceQueue这个“监听器”来优雅的实现自动删除那些引用不可达的key的。关于ReferenceQueue会在下篇博文中进行介绍
具体如下:
WeakHashMap是通过数组table保存Entry(键值对);每个Entry实际上就是一个链表来实现的。当某“弱键”不再被其它对象引用,就会被GC回收时,这个“弱键”也同时被添加到ReferenceQueue队列中。当下一步我们需要操作WeakHashMap时,会先同步table、queue,table中保存了全部的键值对,而queue中保存的是GC回收的键值对;同步他们,就是删除table中被GC回收的键值对。
源码中完成“删除”操作的函数代码如下:
/** * Expunges stale entries from the table. *翻译:删除过时的条目,即将ReferenceQueue队列中的对象引用全部在table中给删除掉 *思路:如何删除一个table的节点e,方法为:首先计算e的hash值,接着根据hash值找到其在table的位置,然后遍历链表即可。 */
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;
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
例子说明1:往一个WeakHashMap中添加大量的元素
上面说的可能比较空,比如为什么可以作为缓冲表呀之类,可能看一个实际例子之后我们就可以更好的理解上面的两段话
第一段代码,就是HashMap的应用,往HashMap中存放一系列很大的数据。
public class TestHashMap {
public static void main(String[] args){
Map<Integer,byte[]> hashMap = new HashMap<Integer,byte[]>();
for(int i=0;i<100000;i++){
hashMap.put(i, new byte[i]);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
第二段代码,就是WeakHashMap的应用,往WeakHashMap中存放与上例HashMap相同的数据。
public class TestWeakHashMap {
public static void main(String[] args){
Map<Integer,byte[]> weakHashMap = new WeakHashMap<Integer,byte[]>();
for(int i=0;i<100000;i++){
weakHashMap.put(i, new byte[i]);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
运行上面的两段代码,发现,第一段代码是不能正常工作的,会抛“java.lang.OutOfMemoryError: Java heap space”,而第二段代码就可以正常工作。
以上就说明了,WeakHashMap当系统内存不足时,垃圾收集器会自动的清除没有在任何其他地方被引用的键值对,因此可以作为简单缓存表的解决方案。而HashMap就没有上述功能。
但是,如果WeakHashMap的key在系统内持有强引用,那么WeakHashMap就退化为了HashMap,所有的表项都不会被垃圾回收器回收。
例子说明2:一系列的WeakHashMap,往每个WeakHashMap中只添加一个大的数据
看如下的例子,例子的代码是,在for循环中每次都new一个WeakHashMap对象,且每个对象实例中只添加一个key和value都是大的数组对象。看会出现上面现象???
public class TestWeakHashMap3 {
public static void main(String[] args){
List<WeakHashMap<Integer[][], Integer[][]>> maps = new ArrayList<WeakHashMap<Integer[][],Integer[][]>>();
int totalNum = 10000;
for(int i=0;i<totalNum;i++){
WeakHashMap<Integer[][], Integer[][]> w = new WeakHashMap<Integer[][], Integer[][]>();
w.put(new Integer[1000][1000], new Integer[1000][1000]);
maps.add(w);
System.gc();//显示gc
System.out.println(i);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
上面的运行结果如下:即由于空间不足报异常错误。
/*
* 运行结果:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
* at com.wrh.testhashmap.TestWeakHashMap3.main(TestWeakHashMap3.java:15)
* */
- 1
- 2
- 3
- 4
而如下的代码确能够正常工作,这两段代码的区别在于下面这段代码中调用了WeakHashMap的size()方法。
public class TestWeakHashMap5 {
public static void main(String[] args){
List<WeakHashMap<Integer[][], Integer[][]>> maps = new ArrayList<WeakHashMap<Integer[][],Integer[][]>>();
int totalNum = 10000;
for(int i=0;i<totalNum;i++){
WeakHashMap<Integer[][], Integer[][]> w = new WeakHashMap<Integer[][], Integer[][]>();
w.put(new Integer[1000][1000], new Integer[1000][1000]);
maps.add(w);
System.gc();
for(int j=0;j<i;j++){
System.out.println("第"+j+"个map的大小为:"+maps.get(j).size());
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
可能有人要问了,不是说WeakHashMap具有会自动进行垃圾回收,第一种情况为什么会报OOM异常了,第二种情况会正常工作呢????
首先要说明的是,第一段代码并不是没有执行GC,而是仅对WeakHashMap中的key中的Integer数组进行了回收,而value依然保持。我们先来看如下的例子:将value换成一个小的对象Object,就会证明这一点内容。
public static void main(String[] args){
List<WeakHashMap<Integer[][], Object>> maps = new ArrayList<WeakHashMap<Integer[][],Object>>();
int totalNum = 10000;
for(int i=0;i<totalNum;i++){
WeakHashMap<Integer[][], Object> w = new WeakHashMap<Integer[][], Object>();
w.put(new Integer[1000][1000], new Object());
maps.add(w);
System.gc();
System.out.println(i);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面的代码运行时没有任何问题的,这也就证明了key中的Integer数组确实被回收了,那为何key中的reference数据被GC,却没有触发WeakHashMap去做清理整个key的操作呢??
原因是在于:在进行put操作后,虽然GC将WeakReference的key中的Integer数组回收了,并将事件通过到了ReferenceQueue,但是后续却没有相应的动作去触发WeakHashMap来进行处理ReferenceQueue,所以WeakReference包装的key依然存在在WeakHashMap中,其对应的value也就依然存在。
但是在WeakHashMap中会删除那些已经被GC的键值对在源码中是通过调用expungeStaleEntries函数来完成的,而这个函数只在WeakHashMap的put、get、size()等方法中才进行了调用。因此,只有put、get、size()方法来可以触发WeakHashMap来进行处理ReferenceQueue。
以上也就是为什么上面的第二段代码中调用下WeakHashMap的size()方法之后就不会报异常能正常工作的原因。