1 概述
在 Java 编程中,Map
是一个非常常用的数据结构,其中 HashMap
是最常见的实现之一。然而,在某些高级场景中,我们可能会使用 WeakHashMap
。WeakHashMap
与 HashMap
在大多数行为上相似,但有一个关键区别:WeakHashMap
不会阻止垃圾回收器(GC)回收其 key
对象。本文将深入探讨 WeakHashMap
是如何实现这一特性的。
2 Java中的引用类型
在开始WeakHashMap
之前,我们先要对弱引用有一定的了解。
在 Java 中,引用类型有四种:
- 强引用(Strong Reference):这是我们正常编码时默认的引用类型。如果一个对象到 GC Roots 有强引用可达,那么这个对象就不会被垃圾回收。
- 软引用(Soft Reference):软引用阻止 GC 回收的能力相对较弱。如果一个对象只有软引用可达,那么它会在内存不足时被垃圾回收器回收。
- 弱引用(WeakReference):弱引用无法阻止 GC 回收。如果一个对象只有弱引用可达,那么在下一个 GC 回收执行时,该对象就会被回收掉。
- 虚引用(Phantom Reference):虚引用非常脆弱,它的唯一作用是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。
3 引用队列(Reference Queue)
引用队列是一个重要的概念。一般情况下,当一个对象被标记为垃圾(并不代表已经被回收)后,会加入到引用队列。对于虚引用来说,它指向的对象只有在被回收后才会加入引用队列,因此可以用作记录该引用指向的对象是否已被回收。
4 WeakHashMap 的实现
在Java 8中,WeakHashMap
的 Entry
继承了 WeakReference
,并将 key
作为 WeakReference
引用的对象。以下是 WeakHashMap
的 Entry
类的简化实现:
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;
}
@SuppressWarnings("unchecked")
public K getKey() {
return (K) WeakHashMap.unmaskNull(get());
}
public V getValue() {
return value;
}
public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
K k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
V v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public int hashCode() {
K k = getKey();
V v = getValue();
return Objects.hashCode(k) ^ Objects.hashCode(v);
}
public String toString() {
return getKey() + "=" + getValue();
}
}
在这个实现中,key
被包装在 WeakReference
中,并且 WeakHashMap
还维护了一个ReferenceQueue
,用于记录那些已经被垃圾回收的 key
。
因此WeakHashMap利用了WeakReference的机制来实现不阻止GC回收Key。
5 如何删除被回收的 key
数据
在 WeakHashMap
的 Javadoc 中提到,当 key
不再被引用时,其对应的 key/value
也会被移除。那么 WeakHashMap
是如何实现这一点的呢?
通常有两种假设策略:
- 当对象被回收时,进行通知:这种方式看似更高效,但 Java 中没有一个可靠的通知回调机制。
- WeakHashMap 轮询处理失效的 Entry:
WeakHashMap
采用的就是这种方式。在put
、get
、size
等方法调用时,都会预先调用一个expungeStaleEntries
方法,来检查并删除失效的Entry
。
例如,在 put
方法中:
public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
if (h == e.hash && eq(k, e.get())) {
V oldValue = e.value;
if (value != oldValue)
e.value = value;
return oldValue;
}
}
modCount++;
Entry<K,V> e = tab[i];
tab[i] = new Entry<>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
在 put
方法中,WeakHashMap
会调用 getTable
方法,而 getTable
方法会调用 expungeStaleEntries
方法来清理失效的 Entry
:
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}
以下是 expungeStaleEntries
方法的实现:
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;
}
}
}
}
在这个方法中,WeakHashMap
会遍历 ReferenceQueue
,找到那些已经被垃圾回收的 Entry
,并从 table
中删除这些 Entry
。
6 为什么没有使用通知机制
在 Java 中,没有一个可靠的通知回调机制来通知对象被回收。虽然 finalize
方法可以作为辅助验证手段,但它并不是标准的,不同的 JVM 实现可能会有不同的行为,甚至可能不调用 finalize
方法。因此,WeakHashMap
选择了轮询 ReferenceQueue
的方式来处理被回收的 key
。
7 验证 WeakHashMap
的行为
为了验证 WeakHashMap
的行为,我们可以定义一个简单的 MyObject
类,并重写其 finalize
方法:
class MyObject {
private String id;
public MyObject(String id) {
this.id = id;
}
@Override
protected void finalize() throws Throwable {
System.out.println("Object(" + id + ") finalize method is called");
super.finalize();
}
}
然后编写测试代码:
import java.util.WeakHashMap;
public class WeakHashMapTest {
private static WeakHashMap<MyObject, Integer> weakHashMap = new WeakHashMap<>();
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
weakHashMap.put(new MyObject(String.valueOf(count)), count);
count++;
System.out.println("WeakHashMap size: " + weakHashMap.size());
}
// 强制触发 GC
System.gc();
// 等待一段时间,确保 GC 完成
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次添加元素
weakHashMap.put(new MyObject(String.valueOf(count)), count);
count++;
System.out.println("WeakHashMap size: " + weakHashMap.size());
}
}
运行结果如下:
WeakHashMap size: 1
WeakHashMap size: 2
WeakHashMap size: 3
Object(0) finalize method is called
Object(1) finalize method is called
Object(2) finalize method is called
WeakHashMap size: 1
在这个测试中,我们首先向 WeakHashMap
中添加了三个 Entry
,然后强制触发 GC。在 GC 完成后,WeakHashMap
中的 key
被回收,WeakHashMap
的大小减少到 1。
8 总结
WeakHashMap
通过使用 WeakReference
和 ReferenceQueue
来实现不阻止 GC 回收 key
的功能。它通过轮询 ReferenceQueue
来检测并删除那些已经被垃圾回收的 key
,而不是依赖于不可靠的通知机制。这种设计使得 WeakHashMap
在处理那些可能被垃圾回收的 key
时非常有效。