一直对Java的四种引用的概念比较模糊,平时写代码也没有用过。但是在看到WeakReferenceThreadLocal中的应用之后,就花了一些时间了解了这四种引用的区别,并把关注的重点放在了WeakReference上面。

JDK官方对WeakReference的定义如下

Weak reference objects, which do not prevent their referents from being made finalizable, finalized, and then reclaimed. Weak references are most often used to implement canonicalizing mappings.
Suppose that the garbage collector determines at a certain point in time that an object is weakly reachable. At that time it will atomically clear all weak references to that object and all weak references to any other weakly-reachable objects from which that object is reachable through a chain of strong and soft references. At the same time it will declare all of the formerly weakly-reachable objects to be finalizable. At the same time or at some later time it will enqueue those newly-cleared weak references that are registered with reference queues.

概括一下有三点

  1. 弱引用对象不会阻止其指向的对象被GC回收
  2. 当一个对象是弱可达的,那么GC会原子地清除所有指向该对象的弱引用,并标记该对象为finalizable并在随后将其回收
  3. 刚被清除的弱引用会被加入到ReferenceQueue

如下图所示,我们就可以说对象D是弱可达的
object-kd
三条路径分别是Ref(D)->DA->B->C->Ref(D)->DB->C->Ref(D)->D

那么,WeakReference该如何使用呢?
实际上,我们主要就是利用其不会阻止其指向的对象被GC回收的特性,从而避免发生内存泄漏。
一个典型的应用就是WeakHashMap,它和HashMap的实现非常相似,主要的区别就是WeakHashMapEntry继承了WeakReference,且实际上弱引用的仅仅是key
来看Entry的构造方法

1
2
3
4
5
6
7
8
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;
}

可以看到,value是被强引用的,而keyqueue通过父类WeakReference的构造方法进行注册。

1
2
3
4
5
6
7
8
9
10
// WeakReference的构造方法
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}

// Reference的构造方法
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

key被GC回收之后,GC会将弱引用对象本身(不是key对象)加入到queue当中去。
再来看WeakHashMap中清理过期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
30
31
private void expungeStaleEntries() {
// 遍历queue
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
// e即为要清除的Entry
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
// 下面的过程与HashMap的remove过程类似,不再详细分析
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并不会自动清理过期的Entry,我们来看哪些方法调用了expungeStaleEntries
WeakHashMap
由此可见,WeakHashMap会在其常用的方法被调用时,去检查ReferenceQueue,如果队列中非空则进行清除。

那么问题又来了,WeakHashMap在什么场景下使用?
来看TomcatConcurrentCache的实现,非常巧妙的利用了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
35
36
37
38
39
40
41
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

public final class ConcurrentCache<K,V> {

private final int size;

private final Map<K,V> eden;

private final Map<K,V> longterm;

public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
}

public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
synchronized (longterm) {
v = this.longterm.get(k);
}
if (v != null) {
this.eden.put(k, v);
}
}
return v;
}

public void put(K k, V v) {
if (this.eden.size() >= size) {
synchronized (longterm) {
this.longterm.putAll(this.eden);
}
this.eden.clear();
}
this.eden.put(k, v);
}
}

首先,这是一个线程安全的Cache,利用到了ConcurrentHashMapWeakHashMap,分别代表edenlongterm(这个设计和JVMGC的分代收集设计十分相似),并在操作WeakHashMap对象时进行同步,从而保证线程安全。
分析一下,当Cache中元素的个数大于上限size时,此时再往Cacheput,会把ConcurrentHashMap类型的eden中的元素全部复制到WeakHashMap类型的longterm中,并清空eden,再将键值对puteden中去。get也是先从eden中获取,获取不到再到longterm中获取,如果longterm中能获取到,就把这个键值对放到eden中。到这里我们就明白了,这个ConcurrentCache就是一种LRU Cache,它将最近没有被使用的数据放在WeakHashMap中,利用其特性,如果数据被GC回收,那么Cache就会将这个键值对清除,从而避免长期不使用的数据一直存放在Cache中(强引用)而不被回收从而导致内存泄漏。
ConcurrentCache的设计简洁而高效,赏心悦目。

总结一下,通过以上的分析,我们基本上了解了WeakReferenceWeakHashMap的原理及用法,但本文并没有对WeakReference在GC过程中的表现进行分析,这个过程和JVM有关。至于其他的几种引用类型,网上有许多文章叙述了它们的区别,这里不再赘述。Java中的引用类型在自己平时的代码中确实没有显式的去用到,但是我们用到的工具类常常会借助于它们、利用它们的特性,因此对他们进行适当的了解有助于我们编写出更加高效健壮的代码。

参考资料
Java WeakReference Example