WeakHashMap是Map的一种很独特的实现,从它的名字可以看出,它是存贮弱引用的映射的,先来复习一下Java中的四大引用类型:
- 强引用:我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。强引用的对象垃圾回收器绝不会回收它。当内存空间不足,jvm宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
- 软引用:软引用是当jvm中内存不够的情况下会回收其对象,在内存充足的情况下与强引用别无二样。
- 弱引用:弱引用是只要GC扫描到了弱引用,那么它指向的对象就会不管内存是否充足都会进行回收。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,jvm就会把这个弱引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了弱引用,来了解被引用的对象是否将要被垃圾回收。
- 虚引用:虚引用与没有引用没有什么区别,相当于没有引用指向改对象。虚 引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。
通过几个例子来加深对四大引用类型的理解,先看第一个例子:
String reference="蕾姆";//reference就是一个强引用
SoftReference<String> stringSoftReference=new SoftReference<>(reference);
reference=null;
System.gc();
System.out.println(stringSoftReference.get());//输出:蕾姆
上述"蕾姆"有两个引用指向它,一个是强引用reference,另外一个是软引用stringSoftReference。从代码中可以看出,即便调用了GC,将强引用置为null,也依旧没有回收其指向的对象。再看下一个,为了了解什么时候对象回收了,重写finalize方法:
class Rem{
long[] l=new long[10000];
@Override
protected void finalize(){
System.out.println("小蕾姆被回收了");
}
public String toString(){
return "你好,蕾姆";
}
}
public class Test {
public static void main(String args[]) {
WeakReference<Rem> weakReference=new WeakReference<>(new Rem());
SoftReference softReference=new SoftReference(new Rem());
System.gc();
System.out.println(softReference.get());
}
}
//输出:小蕾姆被回收了
//你好,蕾姆
为了使得GC能找到需要回收的对象,在Rem类里定义了一个很大的数组,方便GC进行回收,从代码可以看出,弱引用指向的对象是被回收了的,而软引用则没有被回收掉。理解了上述代码后,再来看看WeakHashMap类,该类继承自AbstractMap抽象类,与HasnMap类相比,它多一个引用队列:
public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V> {
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;
private int size;
private int threshold;
private final float loadFactor;
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
}
经过上述的引用类型的讨论,很容易的得出结论,queue 里存贮着是需要被GC回收掉的引用。同时需要注意的是HashMap类的键是null的话,从null得出的哈希值是0,所以会存贮在第一个桶中,而WeakHashMap类则不一样,它给键为null值定义了一个Object对象:
private static final Object NULL_KEY = new Object();
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
static Object unmaskNull(Object key) {
return (key == NULL_KEY) ? null : key;
}
因此,键为null时与其他的键就没什么区别了。其中它的节点定义是继承了WeakReference,也就是说它里面保存的所有节点都是弱引用:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
}
在WeakHashMap的各项操作中,比如get()、put()、size()都间接或者直接调用了expungeStaleEntries()方法,以清理弱引用指向的key对象:
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;
//将被清理的键对应的值置为null
e.value = null;
//节点数减一
size--;
break;
}
prev = p;
p = next;
}
}
}
}
那么,在什么场景下可以用到WeakHashMap类呢?可以再存贮很大的值的场景下使用到WeakHashMap类,比如存贮上万个节点时,利用HashMap存贮:
class Rem{
int i;
long[] l=new long[10000];
public Rem(int i){
this.i=i;
}
@Override
protected void finalize(){
System.out.println("小蕾姆被回收了");
}
public String toString(){
return "你好,蕾姆";
}
}
public class Test {
public static void main(String args[]) {
HashMap<Integer,Rem> map=new HashMap<>();
// Map<Integer,Rem> map=new WeakHashMap<>();
for (int i=0;i<10000;i++){
map.put(i,new Rem(i));
}
}
}
会报内存溢出的异常,而用WeakHashMap类来存贮则不会,但会一直显示对象被回收掉。从中我们可以得到WeakHashMap的应用场景,这两段代码比较可以看到WeakHashMap的功效,如果在系统中需要一张很大的Map表,Map中的表项作为缓存只用,这也意味着即使没能从该Map中取得相应的数据,系统也可以通过候选方案获取这些数据。虽然这样会消耗更多的时间,但是不影响系统的正常运行。在这种场景下,使用WeakHashMap是最合适的。因为WeakHashMap会在系统内存范围内,保存所有表项,而一旦内存不够,在GC时,没有被引用的表项又会很快被清除掉,从而避免系统内存溢出。