WeakHashMap概述
WeakHashMap是以弱键实现的基于哈希表的存储映射数据的Map。当JVM对于这些弱键所指向的对象进行了清理回收之后,WeakHashMap会自动有效的将被回收了的映射从map中移除。
引用的相关知识
Java中的引用一共分为四种,分别为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference)和幻影引用(Phantom Reference)。
可以看到java.lang.ref包中有对应的几种:
强引用(Strong Reference)
强引用是java默认实现的引用。JVM会尽可能长时间的保留强引用的存在。当没有任何对象指向它时JVM将会回收。//这是一个强引用 Object obj = new Object();
弱引用(Weak Reference)
弱引用是指当对象没有任何的强引用存在,在下一次的JVM的gc回收的时候它将会被回收。//这里是一个强引用 Object obj = new Object(); //new一个弱引用 WeakReference<Object> weakRef = new WeakReference<Object>(obj); //obj = null后不再有强引用 obj = null; //因为jvm的gc时间不确定所以循环 while(true){ //weakRef.get()当还未被回收的时候返回该对象,否则返回null if(weakRef.get() != null){ System.out.println("is alive"); } else{ System.out.println("is not alive"); break; } }
运行结果:
... ... is alive is alive is alive is alive is alive is alive is not alive
- 软引用(Soft Reference)
软引用和弱引用基本性质一致,区别就是软引用JVM只会在虚拟机内存不足的时候才会去回收软引用,这使得软引用很适合做缓存应用。 - 幻影引用(Phantom Reference)
这种引用类型的get方法无论什么时候都会返回null。它唯一的作用就是可以用来记录对象是什么时候被gc回收的。
问题来了。怎么去记录对象的回收呢?
从上面的图片中包含的class可以看到其中有一个ReferenceQueue的class,翻译成中文就是引用队列,用来干什么?
原来java提供这个引用队列,可以把这个队列当作参数传到引用的构造器中,那么之后当对象被回收之后便会做一件事情,就是把被回收的对象放到这个因哟个队列中去。
利用ReferenceQueue记录对象被回收的例子:
Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<Object>(obj, queue);
System.out.println(obj);
obj = null;
System.gc();
while(true){
//已经被回收
if(weakRef.get() == null){
Object o;
//从队列中取
if((o = queue.poll()) != null){
System.out.println(o);
break;
}
}
}
运行结果:
java.lang.Object@15db9742
java.lang.ref.WeakReference@6d06d69c
WeakHashMap
有了引用以及弱引用的相关概念并且了解HashMap的话WeakHashMap便很容易理解了。本文基于JDK1.8
1. 类定义
public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>
跟HashMap一样继承自AbstractMap并且实现Map接口,与之不同的是HashMap同时还实现了Cloneable, Serializable接口,这意味着WeakHashMap将不支持克隆和序列化。
2. 属性
//默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//节点数组
Entry<K,V>[] table;
//size
private int size;
//阈值
private int threshold;
//加载因子
private final float loadFactor;
//ReferenceQueue用于记录gc回收
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
//修改标志
int modCount;
可以看到基本上和HashMap没有什么太大的出入,明显的区别则是多了一个ReferenceQueue,很明显WeakHashMap就是通过这个引用队列来实现自动清理的。
2. 构造方法
public WeakHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Initial Capacity: "+
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load factor: "+
loadFactor);
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
table = newTable(capacity);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
}
//其他构造参数基本与HashMap相同,省略
这里有一点点与HashMap不同的是,HashMap在确定capacity的算法上有些不同。HashMap是通五次的无符号右移并或运算
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
而WeakHashMap则是通过while循环实现。
同时HashMap的table数组并没有直接在构造器里面初始化,而是在加入元素之后延迟初始化的。
3. Entry节点
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new 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;
}
//......
这里看看构造器,传入了一个ReferenceQueue,似乎根最开始讲的ReferenceQueue记录gc回收很相似的用法。因为Entry继承了WeakReference类,所以现在的Entry对象管理的并不是强引用,而是弱引用也就是WeakReference。所以当被回收的时候自然就将会被这个引用队列所记录下来。之后只需要进行清理操作即可。
3. 关键的方法
get方法。为了能让每次获取map中的元素的时候都能从已经自动清理过后的table数组中获取,所以get方法中加入了对应了清理操作。
public V get(Object key) { Object k = maskNull(key); int h = hash(k); //这个getTable便是获取清理后的table Entry<K,V>[] tab = getTable(); int index = indexFor(h, tab.length); Entry<K,V> e = tab[index]; while (e != null) { if (e.hash == h && eq(k, e.get())) return e.value; e = e.next; } return null; }
如何清理的:
private Entry<K,V>[] getTable() { //expunge是抹去的意思 Stale:陈旧的 expungeStaleEntries(); return table; } private void expungeStaleEntries() { //从队列中出队遍历 for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) x; //通过hash找到index int i = indexFor(e.hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> p = prev; //遍历链表 while (p != null) { //记录下一个节点next 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; } //p记录下一个节点用作遍历,pref则是p的上一个节点 prev = p; p = next; } } } }
这下就很清楚了,原来是遍历队列中被回收的记录,再在table数组以及处理hash冲突的链表中去找到该记录并删除。
初次之外,在resize和size方法调用的时候也使用到了上面的expungeStaleEntries方法。这里不做详细介绍。hash方法
final int hash(Object k) { int h = k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
和HashMap不相同,但是有点复杂….
WeakHashMap的特性
- 存储键值对映射。
- key和value可以为null
- 线程不安全
- 当键被gc回收能够自动清理map中的映射
- 不支持克隆和序列化