平时我们使用最多的数据结构肯定是 HashMap,但是在使用的时候我们必须知道每个键值对的生命周期,并且手动清除它;但是如果我们不是很清楚它的生命周期,这时候就比较麻烦;通常有这样几种处理方式:
由一个线程定时处理,可以是Timer或者ScheduledThreadPoolExecutor;
利用重写LinkedHashMap.removeEldestEntry(),实现 FIFOCache 或者 LRUCache;可以参考我之前写的一篇博客 LinkedHashMap 相关;
利用 WeakHashMap 的特性,如果逻辑比较复杂还可以直接使用Reference;这里可以参考 Reference 完全解读 和 Reference 框架概览;
所以本文将主要介绍WeakHashMap的特性,以及补充一些关于 HashMap 实现的对比;相关 HashMap 的介绍也可以参考 HashMap 相关;
一、使用场景上面也介绍了,WeakHashMap适用于不是非常重要的缓存类似的场景;例如:
WeakHashMap<Object, Integer> map = new WeakHashMap<>(); for (int i = 0; i < 100; i++) { map.put(new Object(), i); } System.out.println(map.size()); // 1 System.gc(); // 2 System.out.println(map.size()); // 3 System.out.println(map.size()); // 4 System.out.println(map.size()); // 5 System.out.println(map); // 6 System.out.println(map.size()); // 7// 打印:
100
100
100
46
{}
0
对于以上的结果你可能和我打印的不一样,WeakHashMap按照语义应该是,当 key 没有强引用指向的时候,会自动清除 key 和 value;我这里先解释它的释放过程,如果你觉得很清晰,那WeakHashMap你就算是掌握了;
首先 for 循环结束的时候,key 已经没用强引用指向了,此时所有的 key 都是弱引用了;
接下来执行1,因为我这里只有一个方法,新生代还有足够的空间,所以不会触发 GC,所以所有的 key 任然在堆里面,所以打印100;
然后手动触发 GC,虽然System.gc();不一定会立即执行,但是我这里只有一个方法,所以肯定会执行 GC,这里可以打开 GC 日志查看,-verbose:gc;因为 所有的 key 都是弱引用,所以referent被致为 null,同时将 key 注册到 ReferenceQueue中;
在执行 3-7 的时候,按语义 map 应该为空;但是将 key 注册到 ReferenceQueue并非原子性一次完成的,所以这里会打印不同的值,没注册完成一个,在 map 进行操作的时候,就会将其移除;
将上面的代码改成多线程分析思路也是一样的,如果你觉得有不清楚的地方可以查看下文;
二、WeakHashMap 源码分析 1. 类定义 public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>可以看到虽然WeakHashMap也是基于哈希表,但是却并非像LinkedHashMap一样是继承于HashMap,并且WeakHashMap也没有实现Cloneable, Serializable两个接口,这是因为WeakHashMap基于WeakReference实现的,弱引用并不建议实现序列化,同时弱引用一般用于不是很重要的缓存,也就没必要实现Cloneable, Serializable两个接口了;
2. 核心方法 private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; 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; } public K getKey() { } public V getValue() { public V setValue(V newValue) { public int hashCode() { public String toString() { } 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; } } } }上面代码所列的ReferenceQueue,Entry,expungeStaleEntries()就是WeakHashMap实现的核心了;这里强烈建议要先看 Reference 完全解读 和 Reference 框架概览 这两篇博客,里面同样的内容我也不会再赘述了;
Entry<K,V> extends WeakReference<Object>, 表明所有的节点都是WeakReference,而 key 则是 referent;
queue,所有 key 使用同一个ReferenceQueue监听器,每当 key 被回收的时候,entry 将会被注册到ReferenceQueue中;
expungeStaleEntries,将注册到ReferenceQueue中的 entry 移除,并将 value 置为 null;WeakHashMap的所有操作都先执行expungeStaleEntries,这样WeakHashMap就实现了自动回收不在需要的 key 和 value;
三、性能对比