浅析WeakHashMap

在Java编程中,Map是非常常用的集合,比如像HashMap这样的具体实现。更高级一点,还可能会使用WeakHashMap,WeakHashMap在Spring和Tomcat源码中都有使用,特别是作为缓存存在时,本文对WeakHashMap作简要分析。

WeakHashMap其实和HashMap大多数行为都是一样的,只是WeakHashMap不会阻止GC回收key对象(不是value),那么WeakHashMap是怎么做到的呢?这就是研究的主要问题。

在开始WeakHashMap之前,我们先要对弱引用有一定的了解。在Java中,有四种引用类型:

  • 强引用(Strong Reference),我们正常编码时默认的引用类型,强应用之所以为强,是因为如果一个对象到GC Roots强引用可到达,就可以阻止GC回收该对象。
  • 软引用(Soft Reference)阻止GC回收的能力相对弱一些,如果是软引用可以到达,那么这个对象会停留在内存上时间更长一些。当内存不足时 垃圾回收器才会回收这些软引用可到达的对象。
  • 弱引用(WeakReference)无法阻止GC回收,如果一个对象时弱引用可到达,那么在下一个GC回收执行时,该对象就会被回收掉。
  • 虚引用(Phantom Reference)十分脆弱,它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。

其中还有一个概念叫做引用队列(Reference Queue):

  • 一般情况下,一个对象标记为垃圾(并不代表回收了)后,会加入到引用队列。
  • 对于虚引用来说,它指向的对象会只有被回收后才会加入引用队列,所以可以用作记录该引用指向的对象是否回收。

WeakHashMap自动丢弃key的时机

当除了自身有对key的引用外,此key没有其他引用那么此时WeakHashMap会自动丢弃此key。

下面用一个实例来说明这个情况:此例子中声明了两个Map对象,一个是HashMap,一个是WeakHashMap,同时向两个map中放入a、b两个对象,当HashMap remove掉 a 并且将a、b都指向null时,WeakHashMap中的a将自动被回收掉。出现这个状况的原因是,对于a对象而言,当HashMap remove掉并且将a指向null后,除了WeakHashMap中还保存a外已经没有指向a的指针了,所以WeakHashMap会自动舍弃掉a,而对于b对象虽然指向了null,但HashMap中还有指向b的指针,所以WeakHashMap将会保留。

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
34
public class WeakHashMapTest {
public static void main(String[] args) {
String a = new String("a");
String b = new String("b");

WeakHashMap<Object, Object> weakHashMap = new WeakHashMap<>();
HashMap<Object, Object> hashMap = new HashMap<>();

hashMap.put(a, "aaa");
hashMap.put(b, "bbb");

weakHashMap.put(a, "aaa");
weakHashMap.put(b, "bbb");

hashMap.remove(a);
a = null;
b = null;

System.gc();

Iterator<Map.Entry<Object, Object>> hashMapIt = hashMap.entrySet().iterator();
while (hashMapIt.hasNext()) {
Map.Entry<Object, Object> next = hashMapIt.next();
System.out.println("[HashMap] key = " + next.getKey() + ", value = " + next.getValue());
}

Iterator<Map.Entry<Object, Object>> weakHashMapIt = weakHashMap.entrySet().iterator();
while (weakHashMapIt.hasNext()) {
Map.Entry<Object, Object> next = weakHashMapIt.next();
System.out.println("[WeakHashMap] key = " + next.getKey() + ", value = " + next.getValue());
}

}
}

程序执行结果:

1
2
[HashMap] key = b, value = bbb
[WeakHashMap] key = b, value = bbb

从程序的运行结果来看,即使没有手动对WeakHashMap进行remove操作,WeakHashMap也将a对象的key给丢弃了。

WeakHashMap自动丢弃key的源码分析

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
34
35
36
37
38
39
40
41
/**
* 引用队列,保存待回收的key
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

private void expungeStaleEntries() {
// 遍历引用队列,逐个进行清理
for (Object x; (x = queue.poll()) != null; ) {
// 通过加锁queue进行删除过期entry的操作
synchronized (queue) {
@SuppressWarnings("unchecked")
// 1.先把从queue中取出的Object类型的数据强制转化为Entry对象e
Entry<K,V> e = (Entry<K,V>) x;
// 2.然后计算此entry在桶的位置(table数组的下标i)
int i = indexFor(e.hash, table.length);

Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
// 3.然后开始遍历entry链表
while (p != null) {
Entry<K,V> next = p.next;
// 4.如果此entry是链表头,设置此entry的后继为新的链表头
if (p == e) {
if (prev == e)
table[i] = next;
// 5.否则将此entry的前序节点的后继指针指向此entry的后继节点
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
// 最后设置被删除的entry的value为null,加速垃圾回收,接着修改size
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

源码总结:WeakHashMap是主要通过expungeStaleEntries这个函数的来实现移除其内部不用的条目从而达到的自动释放内存的目的的。基本上只要对WeakHashMap的内容进行访问就会调用这个函数(访问只是手动调用put、get、remove、size等函数),从而达到清除其内部不再为外部引用的条目。

那么问题就来了,照上面的说法,只有访问map时才调用expungeStaleEntries函数。那么如果预先生成了WeakHashMap,但是在GC以前又不曾访问该WeakHashMap,是不是就不能释放内存了呢?

通过下面的两个测试案例,就会得出这个问题的答案。将JVM内存设置为64M:-Xms64m -Xmx64m:

案例一

1
2
3
4
5
6
7
8
9
10
11
12
public class WeakHashMapTest1 {
public static void main(String[] args) throws Exception {
List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<>();
d.put(new byte[1000][1000], new byte[1000][1000]);
maps.add(d);
System.gc();
System.err.println(i);
}
}
}

运行结果:

1
2
3
4
5
6
7
8
52
53
54
55
56
57
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.bonree.java.collection.WeakHashMapTest1.main(WeakHashMapTest1.java:20)

案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WeakHashMapTest2 {
public static void main(String[] args) throws Exception {
List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<>();
d.put(new byte[1000][1000], new byte[1000][1000]);
maps.add(d);
System.gc();
System.err.println(i);
for (int j = 0; j < i; j++) {
System.err.println(j + " size" + maps.get(j).size());
}
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
990 size0
991 size0
992 size0
993 size0
994 size0
995 size0
996 size0
997 size0
998 size0

Process finished with exit code 0

从两次运行结果来看,案例一跑不了几步循环就内存溢出了。果不其然,WeakHashMap这个时候并没有自动帮我们释放不用的内存。而案例二一直运行到for循环结束也没有出现内存溢出的问题。所以可以有如下总结:

WeakHashMap并不是你啥也干他就能自动释放内部不用的对象的,而是在你访问它的内容的时候执行expungeStaleEntries函数释放内部不用的对象。

使用WeakHashMap时的注意事项

注意点一:

常量数据存储在WeakHashMap中,无论其外部key的引用是否为null,都不会在weakhashMap中被清除,只有手动remove才会被清除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WeakHashMapTest {
public static void main(String[] args) throws Exception {
WeakHashMap<Object, Object> weakHashMap = new WeakHashMap<>();
String a1 = "a1";
String a2 = "a2";
String a3 = new String("a3");
weakHashMap.put(a1, "a1");
weakHashMap.put(a2, "a2");
weakHashMap.put(a3, "a3");
System.out.println(weakHashMap);

a1 = null;
a3 = null;
System.gc();
System.out.println(weakHashMap);
}
}

运行结果:

1
2
{a3=a3, a2=a2, a1=a1}
{a2=a2, a1=a1}

String a1 = “a1” 是存储在常量池中的。即使a1=null, WeakHashMap中的a1键值对还是会存在,因为a1引用的指向的内存区域数据还是存在,通俗讲就是a1指向的是常量池,GC不会回收常量池中的内容。所以WeakHashMap不会影响指向常量数据的引用。

注意点二:

WeakHashMap是线程不安全的,多线程场景下可以使用Collections工具类将其包装成线程安全的。

1
2
WeakHashMap<String, String> weakHashMap = new WeakHashMap<String, String>();
Map<String, String> intsmaze = Collections.synchronizedMap(weakHashMap);
坚持原创技术分享,您的支持将鼓励我继续创作!

------本文结束 感谢您的阅读------