FinalReference 源码解读

缘起

【2】中我们已经了解到了FinalReference这种引用类型. 我们在【2】中简要分析了一下该类的子类Finalizer和Object的finalize方法之间的关系, 并得出了尽量不要覆写finalize方法的结论,因为如果你的finalize方法执行时间过长的话,由于Finalizer作为Reference的子类,referent属性迟迟得不到释放(未变成null,强引用一直存在),导致不能被后续回收,可能最终导致OOM. 本文对FinalReference特别是它的子类——Finalizer进行更加细致的分析——分析它和垃圾回收,和finalize方法之间的关系. 本文参考了【1】

分析

首先,FinalReference的全部代码是极其简单的.

1
2
3
4
5
6
7
8
9
/**
* Final references, used to implement finalization
*/
class FinalReference<T> extends Reference<T> {

public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

​ 源码1

注释写的很明确——FinalReference 就是用于执行对象的finalize方法的.

关于FinalReference 的父类Reference,我在【3】中已经分析过了. 下面我们来到更加感兴趣的FinalReference 的子类Finalizer.

我们来看看它的属性

1
2
3
4
5
6
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();
private Finalizer
next = null,
prev = null;

​ 源码2

Finalizer和【1】中讲的Reference(Finalizer的爸爸是FinalReference,而FinalReference的爸爸是Reference)的不同点在于,Finalizer中维护了2个队列. 一根是Finalizer的static的queue(即源码2的第一行),它其实就是Finalizer的爷爷Reference的queue属性(这是一根单向链表,因为Reference中只有next属性,而没有prev属性),一根是源码2中的第二行——unfinalized(表示尚未执行finalize方法的对象组成的链表的头结点),这是一根双向链表,为什么说是双向的? 因为源码2的第五行和第六行有next和prev属性. 注意,源码2的next是private的,这表明在ReferenceQueue中的enqueue方法中对r.queue的访问其实访问到的是Reference中的next,而不是源码2中Finalizer自己私有的next. 所以其实Finalizer有2根链表,一根是Reference中的queue(单向链表,我们下面简称其为Q),一根是自己私藏的unfinalized——用于记录尚未执行finalize方法的对象的双向链表.下面我们简称它为unfinalized. 其实Q在【3】中已经详尽分析过了,本文就不再细说了. 本文主要分析unfinalized队列.

等等~ 为什么说Finalizer中的static queue属性(即源码2的第一行)就是Reference的queue属性呢? 因为我们瞧瞧Finalizer的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private Finalizer(Object finalizee) {
super(finalizee, queue); // referent就是要被GC的对象finalizee,其绑定的队列就是静态属性queue.
add();
}
private void add() { // 头插法维护unfinalized队列
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}

​ 源码3

注意源码3的第六行,其实就是将Reference中的queue属性(默认访问权限,或者称为友元访问权限)赋值为Finalizer的static queue属性. 我们参看Reference的如下构造器就知道了

1
2
3
4
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

​ 源码4

源码3的第六行最终调用的就是源码4. 所以其实每个Finalizer类型实例的爷爷中的queue属性就是Finalizer类型中的静态的queue属性. 也就是每个Finalizer类型的实例都有一个爷爷(一个Finalizer类型的实例的爷爷和另一个Finalizer类型实例的爷爷不一样),而爷爷们的queue属性都指向同一个queue——就是Finalizer类型的静态queue属性(源码1的第一行)对应的堆内存.

注意哈,这里有个性质必须要注意——就是不论是Q还是unfinalized,都是静态的(它们是引用类型的,指向一块堆内存). 这就保证了所有的Finalizer类型的对象共用一个Q(单向链表)、共用一个unfinalized(双向链表). 其实与其说是共用,不如说是一起参与到这两根链表的构建之中. 对于一般的情形,即Reference类型的queue属性不是静态的属性,则就是一个Reference类型对应一个queue.(很奇怪~对吧?)

这两根链表的类型显然都是Reference(即都是引用队列)

那么这两根链表加入元素的时机分别是什么呢? 很遗憾,都是由jvm控制的. 何以为证?

首先通过【1】我们知道

Reference的pending属性的注释是

1
2
List of References waiting to be enqueued.
The collector adds References to this list, while the Reference-handler thread removes them.

而通过【1】我们知道pending+discovered 构成的pending list(pending单向链表)是由jvm的垃圾回收器构建的(你可以去翻看Reference的 discovered属性,它是当前节点在pending list的下一个节点,pending是pending list的头结点,discovered属性的注释赫然写着 “used by VM”). 而Reference 的内部类——ReferenceHandler线程就像是一个尽职尽责的搬运工——不停的从这个GC器维护的pending list中,通过tryHandlePending方法不断的移除pending list的头节点(一个Reference类型的对象,即引用类型的对象)——移动到Q中(当然,前提是这个头结点的类型是Finalizer才会进到Q中去)(这种说法只对于初始化没有指定queue的引用类型成立,则直接进入Inactive状态,而不会进入Enqueued状态),这些在【1】中都有详述. 可以去看看【1】.

而unfinalized呢? 它的新增轨迹参见源码3,一切缘起于源码3的register方法,而这个方法上的注释就是”Invoked by VM”. 所以这两根链表的元素的加入都是起源于JVM——Q的数据源头就是pending list(maintained by GC),而unfinalized干脆就是 invoked by VM(FinalReference引用类型专门为带非空实现的finalize方法的类服务,可以理解为每一个覆写了finalize方法的对象,其都会封装为一种FinalRefernece对象.因为finalize方法是object定义的,其默认实现为空.那么如果重写了此方法,那么方法体肯定不为空.即可以通过这一种区别开来.只要finalize方法实现不为空的类,此产生的对象都需要被包装为FinalReference.).

那么这两根链表之间有什么联系呢? 我个人的理解就是,Q和ReferenceHandler线程有关,而unfinalized和FinalizerThread线程有关(FinalizerThread是Finalizer的内部类,正如ReferenceHandler是Reference的内部类, 后面会细说.)

通过ReferenceHandler线程的tryHandlePending方法从pending list搬来了元素进入了Q. 而unfinalized不断的被FinalizerThread消费——调用着unfinalized队列中的元素(Finalizer类型的实例)对应的referent属性(确切讲,是每个unfinalized队列中的元素的Reference爷爷的这个属性,即就是要GC掉的对象的强引用) 的finalize方法. 所以到此时为止,unfinalized队列中的每个元素的referent属性是没有置null的——因为在unfinalized队列的消费过程中还有用啊~ 这一点参见【2】最后一幅图也知道了.

但是对于Reference的其他子类则不然,我们有以下结论

  • WeakReference对象进入到queue之后,相应的referent为null.
  • SoftReference对象,如果对象在内存足够时,不会进入到queue,自然相应的reference不会为null.如果需要被处理(内存不够或其它策略),则置相应的referent为null,然后进入到queue.
  • FinalReference对象,因为需要调用其finalize对象,因此其reference即使入queue,其referent也不会为null,即不会clear掉.
  • PhantomReference对象,因为本身get实现为返回null.因此clear的作用不是很大.因为不管enqueue还是没有,都不会清除掉.

好了,原理已经讲清楚了,我们来看看源码. 弄清楚了上述原理,源码其实看起来很简单的

注意源码2的第三行——我们有一个lock属性,其实为什么需要锁? 这把锁作用在于锁定unfinalized队列. 这个队列可能被当前线程操作,也可能被JVM线程操作(为什么? 你没看到源码3 的第一行的”Invoked by VM”么? ),其实很好理解啦,就是一根生产者线程,一根消费者线程嘛~ 并发操作unfinalized队列,那unfinalized队列当然需要一把锁啦~

生产者线程已经分析完毕了. 下面分析一下消费者线程. 即Finalizer的私有静态内部类——FinalizerThread.

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
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
// in case of recursive call to run()
if (running)
return;

// Finalizer thread starts before System.initializeSystemClass
// is called. Wait until JavaLangAccess is available
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}

​ 源码5

其中第23行明显就是一个死循环——不断地从unfinalized队列中取出Finalizer类型的对象,然后调用其runFinalizer方法. “取出”的源码如下

java.lang.ref.ReferenceQueue.remove(long) 注意,这是爷爷Reference的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Reference<? extends T> remove(long timeout)
throws IllegalArgumentException, InterruptedException
{
if (timeout < 0) {
throw new IllegalArgumentException("Negative timeout value");
}
synchronized (lock) {
Reference<? extends T> r = reallyPoll();
if (r != null) return r;
long start = (timeout == 0) ? 0 : System.nanoTime();
for (;;) {
lock.wait(timeout);
r = reallyPoll();
if (r != null) return r;
if (timeout != 0) {
long end = System.nanoTime();
timeout -= (end - start) / 1000_000;
if (timeout <= 0) return null;
start = end;
}
}
}
}

​ 源码6

跟进源码6的第13行,

java.lang.ref.ReferenceQueue.reallyPoll() 这里是 ReferenceQueue 的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Reference<? extends T> reallyPoll() {       /* Must hold lock */
Reference<? extends T> r = head;
if (r != null) {
@SuppressWarnings("unchecked")
Reference<? extends T> rn = r.next; // 注意这里的next是Reference的友元属性
head = (rn == r) ? null : rn; // 采用抹去头结点的方式进行移除
r.queue = NULL; // 这里的queue原本指向Reference中的queue,也就是Finalizer中的静态queue,也即是Q队列,现在指向了NULL, 也就是单纯的移情别恋
r.next = r; // 这里访问的next是Q中的next,而不是unfinalized中的next
queueLength--;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
return r;
}
return null;
}

​ 源码7

注意源码7的第5行,此时r是Finalizer类型的对象. 而这里的next属性就是它爷爷Reference的属性(而不是Finalizer自己的私有的next属性(这是用于维护Finalizer的unfinalized队列的指针),因为在ReferenceQueue中是看不到Finalizer的私有的next属性的)。我们再注意源码7的第7行,r.queue=NULL.

而NULL是

java.lang.ref.ReferenceQueue

1
2
3
4
5
6
static ReferenceQueue<Object> NULL = new Null<>();
private static class Null<S> extends ReferenceQueue<S> {
boolean enqueue(Reference<? extends S> r) {
return false; // 不允许加入元素
}
}

​ 源码8

注意,源码7的第七行的r.queue访(指)问(向)的是Reference中的queue属性,根据前面的分析就是Finalizer中的静态属性queue. 但是这里赋值为NULL 并不代表 Finalizer中的queue属性变成了NULL队列,因为这只是单个Finalizer实例对象自己的queue指针移(指)情(向)别(别)恋(处)了而已, 即它脱离了Q队列. 所以没毛病.

我们继续分析源码5的第26行.

java.lang.ref.Finalizer.runFinalizer(JavaLangAccess)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);

/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}

​ 源码9

注意源码9第二行使用的锁是this锁,而不是lock锁. 因为这里并不涉及unfinalized队列的操作(涉及unfinalized队列操作的第四行的remove会在其自己的方法中去拿lock锁的).

跟进源码9的第三行

1
2
3
private boolean hasBeenFinalized() {
return (next == this);
}

​ 源码10

是根据next==this来判断它有没有执行finalize方法的. 为什么这样可以判断是不是执行过finalize方法呢? 因为你看看如果执行过finalize方法的话会做什么事情. 看看源码9的第三行.

java.lang.ref.Finalizer.remove()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void remove() {
synchronized (lock) {
if (unfinalized == this) {
if (this.next != null) {
unfinalized = this.next;
} else {
unfinalized = this.prev;
}
}
if (this.next != null) {
this.next.prev = this.prev;
}
if (this.prev != null) {
this.prev.next = this.next;
}
this.next = this; /* Indicates that this has been finalized */
this.prev = this;
}
}

​ 源码11

你看,JDK的作者都已经写了注释——“Indicates that this has been finalized”. 所以源码11的remove的作用就是从unfinalized队列中移除当前Finalizer对象实例. 源码9的第七行调用了Reference的get方法.

java.lang.ref.Reference.get()

1
2
3
public T get() {
return this.referent;
}

​ 源码12

看到了么? 返回的是referent. 也就是Reference中的referent属性. 所以这就应验了上面说过的——到此之前,此Finalizer对象的实例的爷爷Reference的referent属性是不会被置null的. 不然的话,这里返回的就是null. 则源码9的第九行就不会执行——也就是finalizee的finalize方法不会被回调.

从源码9的第八行知道——如果finalizee不为空并且它不是枚举类的话,就会调用它的finalize方法(源码9的第九行). 何以为证? 跟进源码9的第九行

java.lang.new JavaLangAccess() {…}.invokeFinalize(Object)

1
2
3
public void invokeFinalize(Object o) throws Throwable {
o.finalize();
}

​ 源码13

可见,就是执行finalizee(即将被gc掉的对象的强引用)的finalize方法. 最后源码9第13行将finalizee置null,最后第16行调用了super.clear(), 即

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;
}

​ 源码14

可见,最后将referent置null了. 源码9我们就分析完了. 可见FinalizerThread线程是一根用于不断执行待GC的对象的finalize方法的线程, 这样应验了如果一个对象的finalize方法写的比较耗时的话,将导致极为低效的GC的(参见【2】). 最后我们来看看源码5的FinalizerThread是怎么启动的呢?

我们找到Finalizer的如下static块

1
2
3
4
5
6
7
8
9
10
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2); // MAX_PRIORITY=10(线程最高优先级)
finalizer.setDaemon(true);
finalizer.start();
}

​ 源码15

可见FinalizerThread线程是一根优先级还不错(为8)的后台线程. FinalReference的字节码对象一旦被加载则线程就启动了. 何以为证? 我们可以使用jstack命令看到如下日志

参考

【1】https://www.iflym.com/index.php/code/201609050001.html

【2】https://yfsyfs.github.io/2019/06/28/%E4%B8%BA%E4%BB%80%E4%B9%88%E5%B0%BD%E9%87%8F%E4%B8%8D%E8%A6%81Override-finalize%E6%96%B9%E6%B3%95/

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