EZLippi-浮生志

WeakHashMap原理分析

在比较简单的应用场景下我们常用HashMap来缓存一些数据,比如建立Socket连接时需要保存Socket关联的用户,这个信息无法直接添加到Socket对象的属性中,那就可以通过Map来保存这些元数据信息。但这样子会产生一个问题,对象元数据的生命周期需要与套接字(Socket)的生命周期挂钩,除非你准确地知道应用程序什么时候不再需要这个套接字,并记住从Map中删除相应的映射,否则,Socket和User对象将会永远留在Map中,远远超过了套接字的生命周期,而且HashMap会对key和value持有一个强引用,这会阻止Socket和User对象被垃圾收集,到最后时间长了可能导致JVM OutOfMemory。

强引用与弱引用

JDK里面提供了4种引用类型:强引用、弱引用、软引用和虚引用(四种引用类型的具体介绍可以参考这里),毫无疑问强引用是使用最多的,我们创建一个对象默认就建立了一个它的强引用,只要对象是强引用可达的,JVM就不会回收这个对象的内存,而弱引用的实现是,当一个对象(这里我们称之为referent)只有弱引用可达时,JVM在下次垃圾回收时会回收这个referent对象的内存,听起来可以解决我们前面说的那个问题。那是不是在创建HashMap时,把key和value都设置成WeakReference就可以了呢,这样子实现起来就复杂了,每次往Map中存放数据都要创建WeakReference,虽然GC会回收对应的Key和Value,但是Map里面还是会残留WeakReference对象,Map的大小也会一直越来越大。好在是JDK提供了一个WeakHashMap类,里面比较好的利用了弱引用和引用队列来及时将不再引用的对象从Map中删除。

引用队列

WeakHashMap的Entry继承自WeakReference,在创建Entry的时候要关联一个ReferenceQueue,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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) {
//从这里可以看出Entry里的key是弱引用,并且和Key关联了引用队列
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
}

那引用队列有什么用呢,引用队列是垃圾收集器(GC)向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取被引用的对象referent作为参数,另一个还取引用队列(ReferenceQueue)作为参数。如果用关联的引用队列referenceQueue创建弱引用,在GC要回收该弱引用引用的对象(由Entry的构造方法可知,WeakHashMap是根据key来决定这个Entry的生命周期的)时,这个引用对象(在这里是指Entry对象)就在引用清除后加入到引用队列中。之后,应用程序从引用队列中获取引用并进行相应的清理活动,如去掉已不在弱集合中的对象的项,在调用WeakHashMap的方法时会调用expungeStaleEntries()来清除在引用队列中的Entry,如下所示:

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
private void expungeStaleEntries() {	
//遍历引用队列
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
//注意引用队列中的是Entry不是Key
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;
//从HashMap中清除该Entry
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;

e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

总结

回到我们最初的问题,当我们创建一个Socket和User的映射表时,如果Socket对象不再使用时,JVM在引用处理时会检查到该Socket对象是只有弱引用可达的,在清除该对象时会把对象的WeakReference对象(注意WeakRefence也是一个对象,该对象内部持有Socket对象的引用)放到引用队列中,当应用程序调用WeakHashMap的方法时会遍历referenceQueue,然后清除对应的不再使用的Entry,这样子就避免了HashMap一直膨胀而导致内存泄露的问题。

🐶 您的支持将鼓励我继续创作 🐶