java.lang.ref.Reference 源码解读

缘起

java.lang.ref.Reference 是java中的引用类型对象. 它与GC机制密切相关. 所以我们打算从源码角度理解一下它. 本文参考了【1】和【2】

分析

在【4】中我们知道软引用、弱引用、虚引用都和一个引用类型队列(ReferenceQueue)相关(软引用和弱引用可以不和引用队列相关,但是虚引用必须和引用队列相关,这一点在【4】中已经说过了). 而且说法是一旦”被引用的对象的可达性发生变化的时候,并且符合某种回收策略,jvm就会把这个引用加入到与之关联的引用队列中”

那么从java源码的角度,这一切是怎么发生的呢? 我们来探究一下Reference的源码.

首先Reference是java.lang.ref包下的抽象类. 它的类的继承结构如下

关于FinalReference我们在【3】中已经详述了. 关于其他3种引用,我们在【4】中也分析过了. 本文主要精力只是分析Reference本身. 注意,Reference是不包含强引用类型的.

我们首先来看看JDK官方给它下的定义

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
42
43
44
45
46
A Reference instance is in one of four possible internal states:

Active: Subject to special treatment by the garbage collector. Some
time after the collector detects that the reachability of the
referent has changed to the appropriate state, it changes the
instance's state to either Pending or Inactive, depending upon
whether or not the instance was registered with a queue when it was
created. In the former case it also adds the instance to the
pending-Reference list. Newly-created instances are Active.

Pending: An element of the pending-Reference list, waiting to be
enqueued by the Reference-handler thread. Unregistered instances
are never in this state.

Enqueued: An element of the queue with which the instance was
registered when it was created. When an instance is removed from
its ReferenceQueue, it is made Inactive. Unregistered instances are
never in this state.

Inactive: Nothing more to do. Once an instance becomes Inactive its
state will never change again.

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 = this

Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
in queue, or this if at end of list.

Inactive: queue = ReferenceQueue.NULL; next = this.

With this scheme the collector need only examine the next field in order
to determine whether a Reference instance requires special treatment: If
the next field is null then the instance is active; if it is non-null,
then the collector should treat the instance normally.

To ensure that a concurrent collector can discover active Reference
objects without interfering with application threads that may apply
the enqueue() method to those objects, collectors should link
discovered objects through the discovered field. The discovered
field is also used for linking Reference objects in the pending list.

这里多说一句,虽然有的时候看大段英文会让人感到不适(毕竟现在是快餐文化时代),但是最直接,第一手的资料就是从它的官方文档获取的.

译文如下(结合我自己的理解,我可不会懒到直接Google翻译,然后拿着翻译的狗屁不通的文字来糊弄读者,我一直都是坚持使用大白话,也就是俗称的人话来进行解释的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
一个Reference类型的对象实例(简称R)总体来说有四种状态
1. Active状态
即R的referent属性指向的(堆中)对象(下简称r)还有除了referent之外的其他强引用,尚未回收. 处于active状态的R会被GC器特别对待,因为GC器会监视它.一旦r的可达性发生了变化,变化到了某种状态以至于符 合回收策略的话,则R会改变它的状态到pending或者inactive.至于到底是变化到pending还是inactive, 这取决于当时R创建的时候有没有传入ReferenceQueue(引用队列,本文简称RQ)这个参数进入构造器(参看下面的源码2, 即是否使用的是源码2的第七行的那个构造器,简称构造器2). 如果使用了构造器2的话,就会将R加入到pending list(也就是Reference类中的static属性pending,它是该pending list的头结点),新创建的Reference对象实例都是处于active状态的.

2. Pending状态
即表示该R已经是pending list的一员了. 就等着ReferenceHandler线程把它移入相应的ReferenceQueue了。注意,如果R当时创建的时候没有调用构造器1的话,则R永远不会处于本状态.
3. Enqueued状态
即表示该R已经是ReferenceQueue的一员了. 这个ReferenceQueue就是当时创建R时调用构造器2时传入的第二个参数(也就是R的成员变量queue). 当R离开ReferenceQueue的时候,R的状态就变成了inactive状态(也就是下面的第四种状态). 注意,和Pending状态一样,对于调用构造器1创建的R而言,是永远不会处于本状态的.
4. Inactive状态
不会再做任何事情了, 一般的,如果R变成了本状态的话,它的状态将永远不会改变.

如果Reference实例处于上述4种不同状态下,它的next属性和queue属性将有不同的值
1. 处于Active状态下的话,queue就是创建R时调用构造器2的时候传入的queue(如果创建R是通过调用构造器1来创建的话,则queue是ReferenceQueue.NULL). next是null
2. 处于Pending状态下的话,queue是构造器2时时传入的queue, 而next是this(毕竟参与刻画pending list的是discovered属性,而不是next, next是刻画RQ的)
3. 处于Enqueued状态的话,则queue属性是ReferenceQueue.ENQUEUED,next是本Reference对象实例在RQ中的下一个元素, 如果本Reference对象实例是RQ中最后一个元素的话,则next=this
4. 处于Inactive状态的话, 则queue等于ReferenceQueue.NULL; 而next属性 = this

有了上面不同状态下queue、next的不同,GC器很容易通过检测一个Reference实例对象的next属性来看看它是不是处于Active状态, 如果next属性是null的话,则它处于Active状态,否则就是处于其他三种状态. 为什么要区分Active和其他三种状态? 因为Active状态的话,GC器是要监视它的~ 所以要区别对待.

我想,上面的解释中除了ReferenceQueue和Reference之间的关系之外,其他解释的已经很清楚了. 关于他俩的关系,是比较微妙的. 在后文将细说.

首先看看Reference中的各个属性

1
2
3
4
5
6
7
private T referent;
volatile ReferenceQueue<? super T> queue;
volatile Reference next;
transient private Reference<T> discovered;
static private class Lock { }
private static Lock lock = new Lock();
private static Reference<Object> pending = null;

​ 源码1

referent就是被引用关联的那个堆中的对象的强引用(即相应的Reference也会对堆中对象进行强引用).

queue看上去貌似就是本Reference实例对象要加入的队列, 但其实并不是这么回事,它仅仅是一个标记而已——表明本Reference实例对象有没有加入到引用队列中去. 真正维护队列的是Reference的next属(指)性(针),所以其实真正的ReferenceQueue(下简称RQ)是一个隐藏的、但是真实存在的对象. queue中维护的仅仅是该引用队列的头结点(Reference的head属性就是RQ的头结点). 这个后面细说.

next就是本Reference对象实例在RQ中的下一个节点的引用(当然,前提是本Reference实例已经在RQ中了, 即本Reference对象实例处于Enqueued状态).

discovered就是本Reference对象实例在pending list(一根由GC器维护的链表)中的下一个节点的引用(当然,前提是本Reference实例处于Pending状态)

lock是pending list的锁. 因为不仅仅有ReferenceHandler线程(搬运工,从pending list中移除元素到RQ中去)还有GC线程来操作pending list,即一个生产者,一个消费者,所以自然要加锁.

pending就是pending list的头结点,注意pending是静态属性,所以全局唯一. 即只有一根pending list链表,而且该链表是由GC器维护的.

下面看看Reference对象的构造器

1
2
3
4
5
6
7
8
9
10
/* -- Constructors -- */

Reference(T referent) { // 构造器1
this(referent, null);
}

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

​ 源码2

显然,我们只需要看源码2的第七行就好了. 因为构造器1只是裸调构造器2. 而如果queue是null的话(即没有传入引用队列),则Reference实例的queue属性就是ReferenceQueue.NULL.

而NULL和ENQUEUED是ReferenceQueue的静态常量. 仅仅起到标记作用,而并不能实际存储元素(即起到队列作用)

1
2
3
4
5
6
7
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
private static class Null<S> extends ReferenceQueue<S> {
boolean enqueue(Reference<? extends S> r) {
return false;
}
}

​ 源码3

我们来看如何判定一个元素在RQ中了.

java.lang.ref.Reference.isEnqueued()

1
2
3
public boolean isEnqueued() {
return (this.queue == ReferenceQueue.ENQUEUED);
}

​ 源码4

就是根据Reference对象实例的queue属性是否等于ReferenceQueue.ENQUEUED来判断的.

再来看看置空对被引用堆内存中的对象的引用的方法

java.lang.ref.Reference.clear()

1
2
3
4
5
6
7
8
9
10
/**
* Clears this reference object. Invoking this method will not cause this
* object to be enqueued.
*
* <p> This method is invoked only by Java code; when the garbage collector
* clears references it does so directly, without invoking this method.
*/
public void clear() {
this.referent = null;
}

​ 源码5

方法上的注释写的很明确——断开和被引用的堆内存对象之间的强引用. 但是调用此方法并不会直接导致本引用类型的对象实例进入RQ. 而且这个方法只会被java代码调用(例如Finalizer的runFinalizer方法的最后一行代码,参见【3】),而不会被GC调用,GC可不会那么费事这样来切断和被引用对象的强引用关联。 注意哈~ 这里必须要搞清楚的问题是: 进入RQ的是引用对象实例本身(例如SoftReference、WeakReference等),所以才叫引用队列嘛~,而不是引用对象实例持有的referent强引用(referent引用的就是堆内存中的对象).

再来看如何获取到本Reference实例所持有强引用referent

1
2
3
4
5
6
7
8
9
10
11
/**
* Returns this reference object's referent. If this reference object has
* been cleared, either by the program or by the garbage collector, then
* this method returns <code>null</code>.
*
* @return The object to which this reference refers, or
* <code>null</code> if this reference object has been cleared
*/
public T get() {
return this.referent;
}

​ 源码6

注释说的很清楚——如果强引用被切断——不论是被程序切断的(即源码5)还是通过GC(GC可不会那么费事),则本方法返回的就是null.

最后我们来看我们的搬运工——java.lang.ref.Reference.ReferenceHandler

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
private static class ReferenceHandler extends Thread { // 这是一根线程

private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}

static {
// pre-load and initialize InterruptedException and Cleaner classes
// so that we don't get into trouble later in the run loop if there's
// memory shortage while loading/initializing them lazily.
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}

ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}

public void run() {
while (true) {
tryHandlePending(true);
}
}
}

​ 源码7

ReferenceHandler加载前必须要先保证InterruptedException和Cleaner的字节码对象已经被加载了. 然后跟进我们的核心——源码7的第25行

java.lang.ref.Reference.tryHandlePending(boolean)

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
42
43
44
45
46
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered; // 不断的移除pending list的头结点——pending
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}

// Fast path for cleaners
if (c != null) { /// 如果此次移除的是Cleaner的话,就执行它的clean方法,
c.clean();
return true; // 不继续后面的事情了(也就是pending list中的Cleaner是不会进队的,尽管Cleaner是PhantomReference的子类,肯定初始化的时候绑定了队列,但是也不会进入队列了.)
}

ReferenceQueue<? super Object> q = r.queue; // 获取到从pending list中移除的对象绑定的queue(即RQ)
if (q != ReferenceQueue.NULL) q.enqueue(r); // 如果不是出于Active(即创建的时候调用的是构造器1)或者Inactive状态的话(看看我之前对API注释的翻译),就进队.
return true;
}

​ 源码8

我们通过上面的源码知道了pending list中的Cleaner需要区别对待. 除此之外,我们看看源码8的第44行

java.lang.ref.ReferenceQueue.enqueue(Reference<? extends T>)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
synchronized (lock) {
// Check that since getting the lock this reference hasn't already been
// enqueued (and even then removed)
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) { // 只要是处于的状态不是Pending, 我就直接返回入队失败
return false;
}
assert queue == this; // 断言通过r.queue就是当前队列,即Reference实例对象来到的就是自己的queue属性对应的队列.
r.queue = ENQUEUED; // 更改r的queue属性为ENQUEUED(其实就是标记Reference 对象实例的状态已经更改为入队了),这里并没有真正的做入队操作
r.next = (head == null) ? r : head;
head = r; // 头插法,将r插入head为头结点的链表中(就是前面说的隐藏的、但真实存在的RQ),这里才是真正的执行入队操作——通过next属性
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}

​ 源码9

通过源码9我们就明白了,其实ReferenceQueue并不是真正的RQ, 它只持有RQ的头结点(即ReferenceQueue的head属性, 所以每个Reference对象的实例都有一个ReferenceQueue属性queue,然后这个queue属性都持有一个head属性,这个head属性指向同一块堆内存,就是RQ的头结点.). 每个Reference对象初始化的时候都有自己的queue属性——都绑定了一个队列吗? 非也~ 否则岂不是你创建了多少个Reference对象实例我就要创建多少个队列? 事实上queue属性是仅仅是记录两件事情的结构体:

  1. RQ的头结点(因为插入的时候是头插,所以每个Reference实例对象都需要知道当RQ当前的头结点是什么)
  2. 当前Reference实例对象的入队情况——如果queue属性=ReferenceQueue.NULL的话,表明XXX, 如果queue属性=ReferenceQueue.ENQUEUED, 则表明已经入队. 这在判断的时候有作用的.

讲到这里我们就明白了ReferenceHandler线程是干什么的了. 它就是源源不断的将pending list中的对象(他们都处于Pending状态)搬运到RQ中(则状态都改为Enqueued状态). 也就是下图

​ 图1

最后,我们来看看ReferenceHandler线程是怎么创建的, 我们来看看Reference的如下静态代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY); // MAX_PRIORITY=10 最高优先级
handler.setDaemon(true);
handler.start();

// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}

​ 源码10

可见,线程处于最高优先级,是一根后台线程.在Reference字节码对象加载时就会加载完毕的.

我们可以使用jstack追踪到这根后台线程的身影

img

综上,我们完成了Reference的源码分析.

参考

【1】https://blog.csdn.net/zqz_zqz/article/details/79474314

【2】https://www.iflym.com/index.php/code/201609050002.html

【3】https://yfsyfs.github.io/2019/07/05/FinalReference-%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/

【4】https://yfsyfs.github.io/2019/06/28/Java%E4%B8%AD%E7%9A%84-%E5%BC%BA%E5%BC%95%E7%94%A8%E3%80%81%E8%BD%AF%E5%BC%95%E7%94%A8%E3%80%81%E5%BC%B1%E5%BC%95%E7%94%A8%E3%80%81%E8%99%9A%E5%BC%95%E7%94%A8/