定义
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> { }
WeakHashMap实现了Map接口,它比HashMap多了一个引用队列:
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
为了搞清WeakHashMap的原理,必须先搞清楚ReferenceQueue这个引用队列和其引用的对象Reference:
Java中的引用类型
强引用(Strong Reference):强引用是Java中实例化对象采用的默认的引用类型,如“Object o = new Object()”这类的强引用,只要强引用存在,垃圾收集器就不会回收掉被引用的对象。
软引用(SoftReference):软引用用来描述一些还有用但并非必需的对象。对于软引用指向的对象,在系统将要发生内存溢出之前,会将这些对象所占的内存回收,如果回收之后仍然没有足够的内存,才会抛出内存溢出异常。
弱引用(WeakReference):弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用指向的对象只能生存到下一次垃圾收集发生之前,一旦垃圾收集器开始工作,无论当前内存是否足够,都会回收掉被弱引用指向的对象。
虚引用(PhantomReference):虚引用是最弱的一种引用关系。一个对象是否有虚引用对它是否会被回收完全没有影响,我们甚至不能通过虚引用来获得一个对象的实例。设置虚引用的唯一用处就是当该对象被回收时会收到一个系统通知。
其中SoftReference、WeakReference、PhantomReference都是Reference的子类。
Reference源码分析
Reference是引用对象的抽象基类。此类定义了常用于所有引用对象的操作。因为引用对象是通过与垃圾回收器的密切合作来实现的,所以不能直接为此类创建子类。
Reference提供了两个构造函数:
Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
一个构造函数带需要注册到的引用队列,一个不带。带queue的意义在于我们可以吃从外部通过对queue的操作来了解到引用实例所指向的实际对象是否被回收了,同时我们也可以通过queue对引用实例进行一些额外的操作;但如果我们的引用实例在创建时没有指定一个引用队列,那我们要想知道实际对象是否被回收,就只能够不停地轮询引用实例的get()方法是否为空了。值得注意的是虚引用PhantomReference,由于它的get()方法永远返回null,因此它的构造函数必须指定一个引用队列。这两种查询实际对象是否被回收的方法都有应用,如weakHashMap中就选择去查询queue的数据,来判定是否有对象将被回收;而ThreadLocalMap,则采用判断get()是否为null来作处理。
接下来是它的主要成员:
private T referent;
在这里我们首先明确一些名词,Reference类也被称为引用类,它的实例 Reference Instance就是引用实例,但是由于它是一个抽象类,它的实例只能是子类软(soft)引用,弱(weak)引用,虚(phantom)引用中的某个,至于引用实例所引用的对象我们称之为实际对象(也就是我们上面所写出的referent)。
volatile ReferenceQueue<? super T> queue; /* 引用对象队列*/
queue是当前引用实例所注册的引用队列,一旦实际对象的可达性发生适当的变化后,此引用实例将会被添加到queue中。
* The state is encoded in the queue and next fields as follows: * * Active: queue = ReferenceQueue with which instance is registered, or * ReferenceQueue.NULL if it was not registered with a queue; next = * null. * * Pending: queue = ReferenceQueue with which instance is registered; * next = Following instance in queue, or this if at end of list. * * Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance * in queue, or this if at end of list. * * Inactive: queue = ReferenceQueue.NULL; next = this. * Reference next;
next用来表示当前引用实例的下一个需要被处理的引用实例,我们在注释中看到的四个状态,是引用实例的内部状态,不可以被外部查看或是直接修改:
-
Active:新创建的引用实例处于Active状态,但当GC检测到该实例引用的实际对象的可达性发生某些适当的改变(实际对象对于GC roots不可达)后,它的状态将会根据此实例是否注册在引用队列中而变成Pending或是Inactive。
-
Pending:当引用实例被放置在pending-Reference list中时,它处于Pending状态。此时,该实例在等待一个叫Reference-handler的线程将此实例进行enqueue操作。如果某个引用实例没有注册在一个引用队列中,该实例将永远不会进入Pending状态。
-
Enqueued: 当引用实例被添加到它注册在的引用队列中时,该实例处于Enqueued状态。当某个引用实例被从引用队列中删除后,该实例将从Enqueued状态变为Inactive状态。如果某个引用实例没有注册在一个引用队列中,该实例将永远不会进入Enqueued状态。
-
Inactive:一旦某个引用实例处于Inactive状态,它的状态将不再会发生改变,同时说明该引用实例所指向的实际对象一定会被GC所回收。
事实上Reference类并没有显示地定义内部状态值,JVM仅需要通过成员queue和next的值就可以判断当前引用实例处于哪个状态:
-
Active:queue为创建引用实例时传入的ReferenceQueue的实例或是ReferenceQueue.NULL;next为null
-
Pending:queue为创建引用实例时传入的ReferenceQueue的实例;next为this
-
Enqueued:queue为ReferenceQueue.ENQUEUED;next为队列中下一个需要被处理的实例或是this如果该实例为队列中的最后一个
-
Inactive:queue为ReferenceQueue.NULL;next为this
Reference对外提供的方法就比较简单了:
public T get() { return this.referent; }
get()方法就是简单的返回引用实例所引用的实际对象,如果该对象被回收了或者该引用实例被clear了则返回null。
public void clear() { this.referent = null; }
调用此方法不会导致此对象入队。此方法仅由Java代码调用;当垃圾收集器清除引用时,它直接执行,而不调用此方法。
clear的方法本质上就是将referent置为null,清除引用实例所引用的实际对象,这样通过get()方法就不能再访问到实际对象了。
public boolean isEnqueued() { return (this.queue == ReferenceQueue.ENQUEUED); }
判断此引用实例是否已经被放入队列中是通过引用队列实例是否等于ReferenceQueue.ENQUEUED来得知的。
public boolean enqueue() { return this.queue.enqueue(this); }
enqueue()方法能够手动将引用实例加入到引用队列当中去。
ReferenceQueue源码分析
ReferenceQueue是引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中。
ReferenceQueue实现了队列的入队(enqueue)和出队(poll),其中的内部元素就是我们上文中提到的Reference对象。队列元素的存储结构是单链式存储,依靠每个reference对象的next域去找下一个元素。
主要成员有:
private volatile Reference extends T> head = null;
用来存储当前需要被处理的节点。
static ReferenceQueue NULL = new Null<>(); static ReferenceQueue ENQUEUED = new Null<>();
static变量NUlL和ENQUEUED分别用来表示没有提供默认引用队列的空队列和已经执行过enqueue操作的队列。
入队逻辑:
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */ synchronized (r) { // 检查reference是否已经执行过入队操作 if (r.queue == ENQUEUED) return false; synchronized (lock) { //将引用实例的成员queue置为ENQUEUED r.queue = ENQUEUED; //若头节点为空,说明该引用实例为队列中的第一个元素,将它的next实例等于this //若头节点不为空,将它的next实例指向头节点指向的元素 r.next = (head == null) ? r : head; //头节点指向当前引用实例 head = r; queueLength++; if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(1); } lock.notifyAll(); return true; } } }
简单来说,入队操作就是将每次需要入队的引用实例放在头节点的位置,并将它的next域指向旧的头节点元素。因此整个ReferenceQueue是一个后进先出的数据结构。
出队的逻辑为:
private Reference<? extends T> reallyPoll() { /* Must hold lock */ if (head != null) { Reference<? extends T> r = head; //头节点指向null,如果队列中只有一个元素;否则指向r.next head = (r.next == r) ? null : r.next; //头节点元素的queue指向ReferenceQueue.NULL r.queue = NULL; //将r.next指向this r.next = r; queueLength--; if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(-1); } return r; } return null; }
总体来看,ReferenceQueue的作用就是JAVA GC与Reference引用对象之间的中间层,我们可以在外部通过ReferenceQueue及时地根据所监听的对象的可达性状态变化而采取处理操作。
比对WeakHashMap和HashMap的源码,发现WeakHashMap中方法的实现方式基本和HashMap的一样,最大的区别在于在于expungeStaleEntries()这个方法,这个是整个WeakHashMap的精髓,我们稍后进行阐述。
它使用弱引用作为内部数据的存储方案。WeakHashMap是弱引用的一种典型应用。
WeakHashMap关键源码分析
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; int hash; Entry<K,V> next; /** * Creates new entry. */ 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; } //其余代码略 }
可以看到Entry继承扩展了WeakReference类。并在其构造函数中,构造了key的弱引用。 此外,在WeakHashMap的各项操作中,比如get()、put()、size()都间接或者直接调用了expungeStaleEntries()方法,以清理持有弱引用的key的表象。
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; } } } }
可以看到每调用一次expungeStaleEntries()方法,就会在引用队列中寻找是否有被清除的key对象,如果有则在table中找到其值,并将value设置为null,next指针也设置为null,让GC去回收这些资源。