强引用 弱引用 软引用 虚引用

强引用(Strong Reference)

我们使用的大部分的引用都是强引用,这是使用最普遍的引用。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。

软引用(SoftReference)

软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。只有在内存不足的时候JVM才会回收该对象。这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。

Object o = new Object();
SoftReference<Object> so = new SoftReference<>(o);
o = null;
System.gc();
System.out.println(so.get());

当我们设置了本地jvm内存后,通过下面代码就可以看到在内存不足时,软引用就会删除内存数据:
ReferenceQueue<byte[]> q = new ReferenceQueue<>();
SoftReference<byte[]> so2 = new SoftReference<>(new byte[13 * 1024 * 1024], q);
System.out.println(so2.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
final byte[] bytes = so2.get();
System.out.println(so2.get());

byte[] b = new byte[13*1024*1024];
System.out.println(so2.get());

弱引用(WeakReference)

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。所以被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。

WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());

一个很好的例子是ThreadLocal中的子类ThreadLocalMap下的Entry对象,他是一个弱引用,而value则是传入的数据。
当传入的value是强引用,而entry回收后key为null,value未被回收,这导致内存一直被占用,造成内存泄漏。

static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
private Entry[] table;

虚引用(PhantomReference)

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。

虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外,所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。

Reference 、ReferenceQueue

ReferenceQueue

引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中。

实现了一个队列的入队(enqueue)和出队(poll还有remove)操作,内部元素就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构( 单向循环链表 )所实现的。

ReferenceQueue名义上是一个队列,但实际内部并非有实际的存储结构,它的存储是依赖于内部节点之间的关系来表达。可以理解为queue是一个类似于链表的结构,这里的节点其实就是reference本身。可以理解为queue为一个链表的容器,其自己仅存储当前的head节点,而后面的节点由每个reference节点自己通过next来保持即可。

属性

volatile Reference<? extends T> head = null;
始终保存当前队列中最新要被处理的节点,可以认为queue为一个后进先出的队列。当新的节点进入时,采取以下的逻辑:

boolean enqueue(Reference<? extends T> r) {
...
r.next = (head == null) ? r : head;
head = r;
...

在获取时:
private Reference<? extends T> reallyPoll() {       /* Must hold lock */
Reference<? extends T> r = head;
if (r != null) {
Reference<? extends T> rn = r.next;
head = (rn == r) ? null : rn;
r.queue = NULL;
r.next = r;
...

long queueLength = 0;队列长度

Lock lock = new Lock()专用于作为锁对象使用以保证Reference(引用)类对象不会出现线程安全问题

static ReferenceQueue<Object> NULL = new Null<>()“空”引用队列
如果一个Reference(引用)类对象没有注册ReferenceQueue(引用队列)类对象或已经从中出队,则会将其queue(引用队列)字段设置为NULL(“空”引用队列),因此可以通过判断Reference(引用)类对象的queue(引用队列)字段是否为NULL(“空”引用队列)来判断其是否注册及出队。

static ReferenceQueue<Object> ENQUEUED = new Null<>()“入队”引用队列
被作为标记值使用。如果一个Reference(引用)类对象已经入队,则其queue(引用队列)字段设置为ENQUEUED(“入队”引用队列),因此可以通过判断Reference(引用)类对象的queue(引用队列)字段是否为ENQUEUED(“入队”引用队列)来判断其是否入队。Null对象保证无法使用入栈功能。

方法

enqueue()入队(false:失败,true:成功)

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
// 同步
synchronized (lock) {
// 获取引用对象所注册的引用队列(快照),并判断引用队列(快照)是否合法。
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
// 断言引用队列(快照)与当前引用队列对象相等。
assert queue == this;
// 设置引用对象所注册的引用队列为"入队"引用队列,表示当前对象已经被加入引用队列。之所以要替换掉引用对象注册的引用队列有两
// 个方面的原因:一是引用对象只允许入队/出队一次,在入队操作完成后替换为"入队"引用队列可以避免二次入队;二是可以通过判断当前
// 引用对象是否处于引用队列中。
r.queue = ENQUEUED;
// 将该节点加入到header,并将之前的header设置为next
r.next = (head == null) ? r : head;
head = r;
queueLength++;
// 如果是最终引用类型,递增终引用的计数。
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
// 唤醒所有处于等待状态中的线程
lock.notifyAll();
return true;
}
}

调用此方法可将Reference(引用)类对象加入队列中。该方法受synchronized关键字保护,因为ReferenceQueue(引用队列)类对象可能会在多线程的环境下使用,因此需要相应的同步手段确保来确保ReferenceQueue(引用队列)类对象中的数据不会出现错误。需要提及的是,enqueue()采用的是头插法,也就是说新入队的Reference(引用)类对象会成为新的头Reference(引用)类对象

poll()出队

public Reference<? extends T> poll() {
// 判断是否为空,此处不仅仅只做一次,在同步环境的reallyPoll中也做了一次检查,双重检查判断是否为空。
if (head == null)
return null;
synchronized (lock) {
return reallyPoll();
}
}

private Reference<? extends T> reallyPoll() { /* Must hold lock */
// 获取头引用(快照)。
Reference<? extends T> r = head;
if (r != null) {
@SuppressWarnings("unchecked")

// 获取头引用对象(快照)的下个引用对象(快照)。
Reference<? extends T> rn = r.next;
// 如果是最后一个与下一个相同,则表示最后一个,否则设置为下一个。
head = (rn == r) ? null : rn;
// 设置头引用对象(快照)的注册引用队列为"空"引用队列,因为该引用对象已经完成了入队/出队操作,引用队列对其已经不在有意义
r.queue = NULL;
r.next = r;
queueLength--;
// 如果是最终引用类型,递减终引用的计数。
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
// 返回出队的头引用对象(快照)。
return r;
}
return null;
}

调用此方法可将ReferenceQueue(引用队列)类对象的头Reference(引用)类对象弹出,当存在头Reference(引用)类对象时返回头Reference(引用)类对象,否则返回null。poll()采用的是头出法,即后继ReferenceQueue(引用队列)类对象会成为新的头ReferenceQueue(引用队列)类对象。通过观察enqueue/poll()方法的运行机制可知,ReferenceQueue(引用队列)类并不是真正的队列,因为队列必须需要保证FIFO(先入先出),而ReferenceQueue(引用队列)类是FILO(先入后出)的运行方式,因此其本质实际是一个栈。

remove(long timeout)/remove()限时移除/无限时移除

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 (;;) {
// 令当前线程陷入阻塞(本质是加入Monitor对象的WaitSet中),直至被执行enqueue()的线程唤醒。
lock.wait(timeout);
// 出队操作,此时可能返回null,因为入队唤醒的是所有阻塞线程,入队的引用对象可能已经被其它线程出队。
r = reallyPoll();
// 如果成功弹出,则返回,表示成功移除。
if (r != null) return r;
// 如果限时不为0,判断限时时间是否到达(限时为0时表示无限尝试直至成功为止)。
if (timeout != 0) {
long end = System.nanoTime();
timeout -= (end - start) / 1000_000;
if (timeout <= 0) return null;
start = end;
}
}
}
}

/**
* Removes the next reference object in this queue, blocking until one
* becomes available.
*
* @return A reference object, blocking until one becomes available
* @throws InterruptedException If the wait is interrupted
*/
public Reference<? extends T> remove() throws InterruptedException {
return remove(0);
}

remove()方法的作用与poll()方法相同,都是将ReferenceQueue(引用队列)类对象的头Reference(引用)类对象弹出并返回,实际上remove()方法底层就是调用poll()方法实现的。两者的区别在于,如果头Reference(引用)类对象不存在,poll()方法会直接返回null,而remove()方法则是会进入阻塞状态,直至有Reference(引用)类对象入队后被唤醒并将之弹出。remove()方法有两个重载,但实际上可以看作一个,因为其中一个是通过调用另一个实现的。

github

Reference

java.lang.ref.Reference 为 软(soft)引用、弱(weak)引用、虚(phantom)引用的父类。

因为Reference对象和垃圾回收密切配合实现,该类可能不能被直接子类化。
可以理解为Reference的直接子类都是由jvm定制化处理的,因此在代码中直接继承于Reference类型没有任何作用。但可以继承jvm定制的Reference的子类。
例如:Cleaner 继承了 PhantomReference
public class Cleaner extends PhantomReference<Object>

构造函数

其内部提供2个构造函数,一个带queue,一个不带queue。其中queue的意义在于,增加注册的queue。

Reference(T referent) {
this(referent, null);
}

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

属性

T referent;应用存储的对象。

volatile Reference next;作为queue的节点,此处指向下一个引用对象。

transient private Reference<T> discovered;被VM使用。

  • 当处于active状态时:discoverd reference的下一个元素是由GC操纵的( 如果是最后一个了则为this );
  • 当处于pending状态:discovered为pending集合中的下一个元素( 如果是最后一个了则为null );
  • 其他状态:discovered为null

private static Lock lock = new Lock();锁,与ReferenceQueue中类似。

static Reference<Object> pending = null;等待被入队的引用列表。收集器会添加引用到这个列表,直到Reference-handler线程移除了它们。这个列表被上面的lock对象保护。这个列表使用discovered字段来连接它自己的元素( 即pending的下一个元素就是discovered对象 )。
jvm在gc时会将要处理的对象放到这个静态字段上面。同时,另一个字段discovered:表示要处理的对象的下一个对象。即可以理解要处理的对象也是一个链表,通过discovered进行排队,这边只需要不停地拿到pending,然后再通过discovered不断地拿到下一个对象赋值给pending即可,直到取到了最有一个。因为这个pending对象,两个线程都可能访问,因此需要加锁处理。

volatile ReferenceQueue<? super T> queue;是对象即将被回收时所要通知的队列。
这里的queue( 即,ReferenceQueue对象 )名义上是一个队列,但实际内部并非有实际的存储结构,它的存储是依赖于内部节点之间的关系来表达。
Reference有4种状态,不同状态的reference其queue也不同:

  • Active:新创建的引用对象都是这个状态,在 GC 检测到引用对象已经到达合适的reachability时,GC 会根据引用对象是否在创建时制定ReferenceQueue参数进行状态转移,如果指定了,那么转移到Pending,如果没指定,转移到Inactive。
  • Pending:pending-Reference列表中的引用都是这个状态,它们等着被内部线程ReferenceHandler处理入队(会调用ReferenceQueue.enqueue方法)。没有注册的实例不会进入这个状态。
  • Enqueued:相应的对象已经为待回收,并且相应的引用对象已经放到queue当中了。准备由外部线程来询问queue获取相应的数据。调用ReferenceQueue.enqueued方法后的Reference处于这个状态中。当Reference实例从它的ReferenceQueue移除后,它将成为Inactive。没有注册的实例不会进入这个状态。
  • Inactive:即此对象已经由外部从queue中获取到,并且已经处理掉了。即意味着此引用对象可以被回收,并且对内部封装的对象也可以被回收掉了( 实际的回收运行取决于clear动作是否被调用 )。可以理解为进入到此状态的肯定是应该被回收掉的。一旦一个Reference实例变为了Inactive,它的状态将不会再改变。

jvm并不需要定义状态值来判断相应引用的状态处于哪个状态,只需要通过计算next和queue即可进行判断。

  • Active:queue为创建一个Reference对象时传入的ReferenceQueue对象;如果ReferenceQueue对象为空或者没有传入ReferenceQueue对象,则为ReferenceQueue.NULL;next==null;
  • Pending:queue为初始化时传入ReferenceQueue对象;next==this(由jvm设置);
  • Enqueue:当queue!=null && queue != ENQUEUED 时;设置queue为ENQUEUED;next为下一个要处理的reference对象,或者若为最后一个了next==this;
  • Inactive:queue = ReferenceQueue.NULL; next = this.

那么针对不同的子类,会得到不同的实现:

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

方法

T get()获取

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

获取当前引用的所指对象,当所指对象不存在时返回null。所指对象初始是必然存在的,但可以在后期被清除。

clear()清除

public void clear() {
this.referent = null;
}

清除当前引用的所指对象(即断开两者的引用关系),并不会将当前引用加入到注册引用队列中。该方法专为开发者提供,GC线程不会调用该方法断开当前引用与其所指对象的关联。

boolean isEnqueued()是否入队

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

判断当前引用是否已加入注册引用队列,是则返回true;否则返回false。引用加入注册引用队列时会将自身注册的引用队列替换为“入队”引用队列,这是一个在引用队列类内部创建的全局静态引用队列,被作为引用加入注册引用队列的标志位来使用。因此判断当前引用是否加入注册引用队列无需遍历注册引用队列,直接判断注册引用队列是否是“入队”引用队列即可。

boolean enqueue()入队

public boolean enqueue() {
return this.queue.enqueue(this);
}

将当前引用加入注册引用队列中,成功返回true;否则返回false。该方法底层调用引用队列类的enqueue(Reference<? extends T> r) 方法实现。该方法专为开发者提供,“引用处理器”线程不会调用该方法将当前引用加入注册引用队列。

四种状态

active ——> pendingReference#tryHandlePending
pending ——> enqueueReferenceQueue#enqueue
enqueue ——> inactiveReference#clear
github

重要功能

ReferenceHandler线程

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

此处会预加载并初始化 InterruptedException 和 Cleaner 类,来避免出现在循环运行过程中时由于内存不足而无法加载它们。然后run方法中使用了while死循环运行tryHandlerPending方法。这个方法通过名字就能大概判断,应该是来处理pending链表的。

tryHandlePending()

static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
// 如果pending链表不为null,则开始进行处理
if (pending != null) {
r = pending;
// 使用 'instanceof' 有时会导致OOM
// 所以在将r从链表中摘除时先进行这个操作
c = r instanceof Cleaner ? (Cleaner) r : null;
// 移除头结点,将pending指向其后一个节点
pending = r.discovered;
// 此时r为原来pending链表的头结点,已经从链表中脱离出来
r.discovered = null;
} else {
// 在锁上等待可能会造成OOM,因为它会试图分配exception对象
if (waitForNotify) {
lock.wait();
}
// 重试
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
// 重试
return true;
} catch (InterruptedException x) {
// 重试
return true;
}

// F如果摘除的元素是Cleaner类型,则执行其clean方法
if (c != null) {
c.clean();
return true;
}

ReferenceQueue<? super Object> q = r.queue;
// 最后,如果其引用队列不为空,则将该元素入队
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}

这里整个过程就是摘取pending链表的头结点,如果是Cleaner,则执行clean操作,否则进行入队处理。

初始化

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);
handler.setDaemon(true);
handler.start();

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

Reference Handler线程会注册到根线程组并设置最高优先级,守护线程并运行线程。最后覆盖了JVM中对pending的默认处理方式。

github

  • 待定列表:待定列表时全局静态变量的方式存在引用类中。其并非实际的列表对象而是逻辑列表。引用类只保存待定列表的头节点,并通过节点之间的引用里访问整个列表。引用加入引用队列之前会暂时与待定列表。
  • 引用队列:用于追踪所指对象的GC状态,当所指对象被GC回收时,其对应引用会被安排加入其注册的引用队列中,由此引用是否处于引用队列可作为其所指对象是否被GC回收的判断依据,并可以此为契机执行某些自定义操作。与待定列表不同,引用队列并不唯一,并且是实际的队列对象。

FinalReference/Finalizer

FinalReference(终引用)类是Reference(引用)抽象类的四大子类之一,被作用于实现Java的Finalization(终结)机制。相比其他三个子类而言终引用类相对鲜为人知,因为本身的封闭性令其无法被开发者直接使用,毕竟其在设计上就是为了后台运行而服务的。
终引用类必须搭配引用队列使用,该特性与PhantomReference(虚引用)类一致。

终引用类最大的特点在于其所指对象的回收时机与其它子类不同。
对于引用抽象类的其它子类,当其所指对象被GC判定为可回收后便会被顺势回收,与此同时引用机制还会将其加入注册引用队列中。换而言之,就是加入到引用队列中的引用的所指对象都可以被视作已GC回收。终引用类与此不同,虽说其在所指对象的可回收判定中并不会造成影响,但即使终引用已经被加入了引用队列,其所指对象也不会被GC回收,而是会等到所指对象的finalize()方法被执行完毕后断开与终引用的关联关系才会被回收。

终引用类的存在是为了实现终结机制而提供对对象生命周期的干预机制,即影响对象的回收时机,而实际真正实现该机制的是其子类Finalizer(终结者),Java终结机制正是通过终结者类与底层逻辑相互配合实现的。

Finalizer(终结者)类是FinalReference(终引用)类的子类,其访问权限为DEFAULT,即只有同一个包下的类才能够进行调用,且其被final关键字修饰,因此可知其无法再被其它子类继承。

final class Finalizer extends FinalReference<Object> {

Finalizer(终结者)类对象会在何时加入未终结链表中呢?F类对象创建时,JVM将之作为所指对象调用Finalizer(终结者)类的register()方法创建一个Finalizer(终结者)类对象并加入到未终结链表中。实际上关于Finalizer(终结者)类对象的加入时间还能有更加精确的描述及调整。众所周知Java对象的创建并不是一个原子操作,其大致可以分为两个部分:实例化(分配内存)与初始化(执行构造方法)。通过对JVM参数-XX:+RegisterFinalizersAtInit的设置(默认为+),可以令Finalizer(终结者)类对象在F类实例化后、初始化前的时间段创建加入(-XX:-RegisterFinalizersAtInit),也可以令其在F类初始化后创建加入(-XX:+RegisterFinalizersAtInit)。

void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) {
ScopedObjectAccess soa(self);
ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));
jvalue args[1];
args[0].l = arg.get();
// 调用 Java 层静态方法 FinalizerReference#add
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
*object = soa.Decode<mirror::Object>(arg.get());
}

JVM是如何做到在指定的时间里调用Finalizer(终结者)类的register()方法的呢?这个就要根据不同的情况判断。针对在实例化后、初始化前调用的F类对象,由于内存分配是统一由内存管理系统负责的,因此可以由其负责处理;而对于在初始化后的F类对象则相对负责,难道要在所有的构造方法尾部都进行处理吗?当然是不可能。实际上JVM使用了一个很灵活的方式,在Java中,子类的构造方法会调用父类的构造方法,因此最终都会执行Object(对象)类的构造方法。JVM将Object(对象)类的构造方法里的return指令替换为_return_register_finalizer指令,从而在侵入性很小的情况下完成了方法调用。

属性

static ReferenceQueue<Object> queue = new ReferenceQueue<>();引用队列
该字段是一个静态字段,其默认了一个ReferenceQueue(引用队列)类的实例,被作为Finalizer(终结者)类对象注册引用队列使用,因此可知所有的Finalizer(终结者)类对象注册的都是同一个引用队列。即,f-queue。

static Finalizer unfinalized = null;未终结的对象
该字段是一个静态字段,被用于保存未终结链表的头Finalizer(终结者)类对象,在整个JVM中只存在一个未终结链表。未终结链表的作用是为了保证Finalizer(终结者)类对象不会被GC回收,如果没有未终结链表会导致Finalizer(终结者)类对象加入引用队列之前不可达(即不存在GC Roots关联的链路),这会使其被GC回收。

static final Object lock = new Object()
用于在操作未终结链表时作为synchronized关键字的锁对象使用。该字段是一个静态字段,因此可知是一个类锁。

private Finalizer next = null, prev = null;前驱终结者/后驱终结者
用于持有当前Finalizer(终结者)类对象在未终结链表中后继/前驱Finalizer(终结者)类对象的引用,因此可知未终结链表是一个双向链表。当Finalizer(终结者)类对象从未终结链表中移除时,会将两者都指向自身,因此可以由此判断一个Finalizer(终结者)类对象是否移除。

静态块

static {
// 获取线程组信息
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());

// 实例化的终结者线程,设置其优先级为8(相对其他守护线程会低一些)且为守护线程,并执行。
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}

静态块中创建了一个FinalizerThread(终结者线程)类对象,也就是所谓的终结者线程,因此可知终结者线程会在Finalizer(终结者)类的类加载过程的初始化步骤完成创建。终结者线程专用于对f-queue中的Finalizer(终结者)类对象进行处理,即执行其所指对象/F类对象的finalize方法。终结者线程是一个守护线程,其优先级为8。

方法

Finalizer(终结者)类只有一个构造方法,通过调用父类FinalReference(终引用)类的构造方法实现。

private Finalizer(Object finalizee) {
super(finalizee, queue);
// 将终结者对象加入未终结链表中。
add();
}

其将queue(引用队列)字段的默认ReferenceQueue(引用队列)类对象作为Finalizer(终结者)类对象的注册引用队列,因此可知所有的Finalizer(终结者)类对象注册的都是同一个引用队列。与此同时该构造方法还会将Finalizer(终结者)类对象加入到未终结链表中。该构造方法是私有的,因此可知Finalizer(终结者)类对象无法直接在类的外部使用new创建。

注册

static void register(Object finalizee) {
new Finalizer(finalizee);
}

即实例化一个Finalizer(终结者)类对象并将之加入注册队列中,是直接调用构造方法实现的。register()是供JVM使用的,因此除非使用反射,否则开发者无法调用该方法。JVM会在F类创建时调用这个方法。

是否终结(false:否,true:是)

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

判断当前Finalizer(终结者)类对象是否已经从未终结链表中移除。终结者对象从终结者链表中移除时,会将前驱终结者/后继终结者设置为自身,因此只需判断是否为自身即可。

新增

private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}

将当前Finalizer(终结者)类对象添加进未终结链表中。该方法采用的是头插法,即新加入的Finalizer(终结者)类对象会成为头Finalizer(终结者)类对象,而原Finalizer(终结者)类对象则会成为其后继Finalizer(终结者)类对象。

移除

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

将当前Finalizer(终结者)类对象从未终结链表中移除。将Finalizer(终结者)类对象从未终结链表中移除并不需要遍历,因为其保存了前驱/后继Finalizer(终结者)类对象的引用,因此只需将前驱/后继Finalizer(终结者)类对象重新建立关联后将自身弹出即可。弹出的同时,需要将next(后继终结者)/prev(前驱终结者)字段指向自身,作为Finalizer(终结者)类对象已经移除的标志。

执行终结者

private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
// 判断当前终结者对象是否已经终结,即判断其是否已从未终结链表中移除。是则直接返回,否则移除。
if (hasBeenFinalized()) return;
remove();
}
// 在catch块中执行终结者对象的所指对象的finalize()方法,目的是确保执行异常时不会中断终结者线程。
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
// 调用所指对象的finalize()方法。
jla.invokeFinalize(finalizee);
// 清除引用,以确保不会被GC错误的遗留。
finalizee = null;
}
} catch (Throwable x) { }
// 清除所指对象与终引用对象(终结者对象)之间的关系,以确保所指对象顺利GC,而在此之前,所指对象不会被回收。
super.clear();
}

该方法用于将Finalizer(终结者)类对象从未终结链表中移除,同时还负责执行其所指对象的finalize()方法,并在finalize()方法执行结束后断开与Finalizer(终结者)类对象的连接以令其可被GC回收。关于上述流程存在以下几个主要注意的点:

  • finalization(终结)机制只保证finalize()方法一定会调用,但不保证会等待其执行结束。这是目前的主流说法,包括《深入理解Java虚拟机》一书中也是这么描述的。虽然从代码上看,似乎会执行完finalize()方法才会断开所指对象与Finalizer(终结者)类对象之间的关联时期被GC回收。但如果主流说法是正确的话,那关于finalize()方法的具体执行一定还有其它的影响条件。
  • finalize()方法只能被执行一次。一个F类对象之所以只能执行一次finalize()方法,是因为其在执行完finalize()方法后会断开其与Finalizer(终结者)类对象的关联。因此,即使在finalize()方法中将F类对象成功复活(即与GC Roots建立上关联),其也不再有Finalizer(终结者)类对象持有它了。一个没有Finalizer(终结者)类对象的F类对象自然是无法执行finalize()方法的。
  • F类对象至少要两次GC才能被真正回收。第一次GC将F类对象判定为可回收并执行finalization(终结)机制机制;第二次GC则会将执行完finalize()方法并断开与Finalizer(终结者)类对象关联的F类对象正式回收。而在这两次GC之间也可能存在有多次的GC,因此是至少两次。

分叉二级终结者

private static void forkSecondaryFinalizer(final Runnable proc) {
// 建一个二级终结者线程执行自定义操作,并等待其完成。
AccessController.doPrivileged(
new PrivilegedAction<Void>() {
public Void run() {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 可以看到这是一个常规的线程,因此所谓的二级终结者线程只是名义上的,并不是真正的终结者线程。
Thread sft = new Thread(tg, proc, "Secondary finalizer");
sft.start();
try {
阻塞主线程。
sft.join();
} catch (InterruptedException x) {
Thread.currentThread().interrupt();
}
return null;
}});
}

该方法用于创建一个名义上二级终结者线程来执行自定义操作,并等待其完成操作。之所以说是名义上的二级终结者线程,是因为其使用的是常规的Thread(线程)类,而不是Finalizer(终结者)类中定义的FinalizerThread(终结者线程类)类。该方法在类中有两处调用:runFinalization()及runAllFinalizers()。前者的执行操作与正常流程相同,相当于新开了一个线程来进行辅助加速;而后者的执行操作则是上文中提及的直接跳过正常流程中引用队列的步骤从未终结链表中获取Finalizer(终结者)类对象并向下执行。

执行终结

static void runFinalization() {
if (!VM.isBooted()) {
return;
}

forkSecondaryFinalizer(new Runnable() {
private volatile boolean running;
public void run() {
// in case of recursive call to run()
if (running)
return;
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
// 从引用队列(f-queue)中取出终结者对象,如果引用队列中不存在终结者对象,则直接结束循环并退出线程。因此
// 可知该方法可有助于加速引用队列及未终结链表中终结者的处理流程。
Finalizer f = (Finalizer)queue.poll();
// 执行终结者对象的所指对象的finalize()方法。
if (f == null) break;
f.runFinalizer(jla);
}
}
});
}

该方法的代码逻辑与正常流程相同,且会调用forkSecondaryFinalizer()方法创建一个二级终结者线程执行。该方法在Runtime(运行)类中存在调用,而该类与程序运行息息相关,因此猜测该方法应该是程序在某些情况下(例如空间内存不足等)用于加速finalization(终结)机制所用,毕竟…两个线程处理肯定会比一个线程快一些。

运行全终结者

static void runAllFinalizers() {
if (!VM.isBooted()) {
return;
}

forkSecondaryFinalizer(new Runnable() {
private volatile boolean running;
public void run() {
// in case of recursive call to run()
if (running)
return;
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
// 直接从未终结链表中获取终结者对象,并将之从未终结链表中移除并执行其所指对象的finalize()方法。当未终结链表
// 为空时退出循环。该操作不会影响主流程,当主流程从引用队列中获取到终结者对象后,会先判断该终结者对象是否已
// 经从未终结链表中移除,因此不会导致所指对象的finalize()方法重复调用。
Finalizer f;
synchronized (lock) {
f = unfinalized;
if (f == null) break;
unfinalized = f.next;
}
f.runFinalizer(jla);
}}});
}

该方法会跳过正常流程中引用队列的步骤从未终结链表中获取Finalizer(终结者)类对象,且会调用forkSecondaryFinalizer()方法创建一个二级终结者线程执行。该方法在Shutdown(关闭)类中存在调用,而从该类的类名猜测,该方法应该是程序关闭前用于紧急处理剩余Finalizer(终结者)类对象所用,毕竟按照正常流程还需要等待GC判定回收F类回收。而如果在程序关闭前没有处理掉所有的Finalizer(终结者)类对象的话,就有可能导致资源泄露的危险(finalization(终结)机制一般都会用来保证资源回收/释放上)。

FinalizerThread

FinalizerThread(终结者线程类)类是Finalizer(终结者)类的一个私有静态内部类,其继承自Thread(线程)类,因此可知其对象为一个线程。

running(是否运行) —— 该字段用于判断finalization(终结)机制是否已经运行,以避免在递归的情况下重复执行…但从源码中似乎没有发现有递归调用的地方。
volatile boolean running;

该方法继承自Thread(线程)类,用于定义线程执行的任务。该方法中定义了finalization(终结)机制的正常流程。

public void run() {
// 如果正在运行,则直接返回,这是为了避免在递归的情况下重复执行。
if (running)
return;

// 终结者线程先于System.initializeSystemClass被调用。等待直到JavaLangAccess可以访问
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
// 将运行状态设置为true,表示已经开始终结工作。
running = true;
for (;;) {
try {
// 从引用队列中获取终结者对象(remove方法是一个阻塞方法,会一直阻塞到能获取到对象为止)。
Finalizer f = (Finalizer)queue.remove();
// 执行终结者对象的runFinalizer()方法。
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}

总结

当F类对象被创建时,JVM会自动将之作为所指对象创建终结者。终结者类对开发者直接屏蔽,即开发者无法直接创建终结者,JVM会在F类对象创建时自动为之创建终结者以进行终结机制。所谓F类,是指finalize()方法被重写且方法体不为空的类,因此虽然所有的类都存在finalize()方法(因为所有的类都继承自Object(对象)类),但JVM并不会为所有的对象创建终结者,同理也并非所有对象的finalize()方法都会被执行。
github
实际上,讲述终结者类的使用其实并没有太大意义,因为终结者类不是公共类,无法被其本身所在包外的类访问,故而开发者并无法使用终结者类。

f-queue

f-queue的直接作用是承载所有的终结者。终结者类与PhantomReference(虚引用)类一样都必须搭配引用对象使用,而终结者创建时都会固定将f-queue作为其注册引用队列,因此最终所有的终结者都会因为Reference(引用)机制加入f-queue中。f-queue的根本作用有二:一是追踪所指对象/F类对象的GC状态,即判断所指对象/F类对象是否已/会被GC回收,这是引用队列的基本功能,此处不在赘述;二是作为JVM执行终结机制的起点,即JVM会从f-queue中取出终结者以执行其所指对象/F类对象的finalize()方法。

未终结链表

未终结链表的作用是建立终结者与GC ROOTS的关联,以确保终结者不会在终结机制期间被GC回收,这与Cleaner(清洁工)类中持有的清洁工链表作用相同。由于终结者由JVM创建,因此初始情况下,如果没有未终结链表,则终结者处于未与GC ROOTS建立直接/间接关联的状态,这就可能出现终结者的所指对象/F类对象尚未执行finalize()方法(更准确的说是终结者加入待定列表前,因为待定链表也是全局静态变量,属于GC ROOTS的范畴,但发现列表不是)就被GC回收的情况,导致部分终结者无法终结。因此为了避免这一点,JVM必须建立所有终结者与GC ROOTS的直接/间接关联,而未终结链表起的就是这个作用。

终结者会在其创建时默认加入未终结链表,因此所有的终结者初始时都存在于未终结链表中。关于上述内容粗略的说法是:当F类对象创建时,JVM会自动将之作为所指对象调用终结者类的register()方法创建终结者并加入未终结链表中。但实际上,关于终结者的加入时间还能有更精确的描述/调整。众所周知Java对象的创建并非原子操作,大致可以分为实例化(内存分配)与初始化(构造方法执行)两个部分。通过对JVM参数-XX:+RegisterFinalizersAtInit的调整 ,可以设置终结者在F类对象实例化后、初始化前加入,也可以设置其在F类对象初始化后加入。

void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) {
ScopedObjectAccess soa(self);
ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));
jvalue args[1];
args[0].l = arg.get();
// 调用 Java 层静态方法 FinalizerReference#add
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
*object = soa.Decode<mirror::Object>(arg.get());
}

JVM是如何做到在指定的时间段里调用终结者类的register()方法的呢?这个就要根据不同的情况判断。针对在实例化后、初始化前调用的F类对象,由于内存分配是统一由内存管理系统负责的,因此可以由其负责处理;而对于在初始化后的F类对象则相对复杂,难道要在所有的构造方法尾部都进行处理吗?当然是不可能。实际上JVM使用了一个很灵活的方式,在Java中,子类的构造方法会调用父类的构造方法,因此最终都会执行Object(对象)类的构造方法。JVM将对象类构造方法里的return指令替换为_return_register_finalizer指令,从而在侵入性很小的情况下完成了方法调用。

将终结者从未终结链表中移除无需遍历。由于未终结链表是双向链表,因此将终结者从未终结链表中移除并不需要遍历,因为终结者自身组合了字段用于保存其在未终结链表中的前驱/后继终结者/节点,因此直接在类锁的保护下将前驱/后继终结者/节点重新链接即可。终结者被移除后,其前驱/后继引用会指向自身,即自引用,作为其已从未终结链表中移除的标志。之所以不使用null作为标志是因为null已经被作为了头/尾终结者/节点的标志,因此可通过判断终结者是否自引用来判断终结者是否已被移除。由于终结者只会在其所指对象/F类对象的finalize()方法执行后才会被JVM从未终结链表中移除,因此其也可以变相作为终结者是否终结的判断依据,而事实上终结者类也确实是这么做的。

终结者线程

终结者线程是终结者类自实现内部类FinalizerThread(终结者线程)类的唯一实例,专用于执行终结机制。终结者线程类是Thread(线程)类的子类,因此其实例同样也是一个线程。终结者线程在终结者类的静态块中创建,是一个优先级为8的守护线程,因此整个JVM中只有一条终结者线程。很多资料都会很简略的介绍说终结机制的执行线程的优先级很低,容易造成终结者/F类对象堆积而导致OOM,但实际上这种说法并不准确,因为相对于大量优先级为5的用户线程来说,优先级为8的终结者线程优先级并不算低。导致OOM的原因更多是因为终结者线程只有一条,难以与数量众多的用户线程竞争CPU资源。

终结者线程用于执行终结机制,简单的说就是不断的将终结者从f-queue及未终结链表中获取/移除终结者,并执行其所指对象/F类对象的finalize()方法,最后断开两者间的引用,以加速两者被GC回收。

终结机制

虽然终结机制确实提供了一套关于某些问题的处理方案,但并不推荐去使用它,因为其是非常不稳定的。正是因为终结机制如此的不靠谱,因此其只能在必要的情况下作为保底机制使用,而不能作为处理问题的常规方案。终结机制可能存在的问题如下:

  • 终结线程的优先级相对较低,finalize()方法的执行速度可能小于终结者/F类对象新增的速度,使得终结者/F类对象累积而导致新生代频繁GC -> 老年代频繁GC -> FULL GC -> OOM;
  • 如果某个F类对象的finalize()方法执行时间很长,会使得终结者/F类对象累积而导致新生代频繁GC -> 老年代频繁GC -> FULL GC -> OOM;
  • JVM/程序运行时,只保证finalize()方法一定会被调用,但不保证会等待其执行结束;
  • JVM/程序退出时,无法保证累积的所有F类对象的finalize()方法都被执行,因此可能导致资源泄露。

终结机制的完整流程如下:

  • F类对象创建,JVM自动将之作为所指对象创建终结者。终结者会被默认加入未终结链表,并且终结者作为终引用也会被默认加入发现列表,故而终结者初始会同时存在于发现列表与未终结链表中,此时终结者处于活跃状态;
  • F类对象被GC判定为可回收,其终结者被GC线程从发现列表中移除并头插至待定列表(堆栈)中,此时终结者处于待定状态;
  • 引用处理器线程将终结者从待定列表(堆栈)中移除并头插至f-queue中,此时终结者处于入队状态;
  • 终结者线程将终结者从f-queue中移除,此时终结者处于怠惰状态;
  • 终结者线程继续将之从未终结链表中移除,随后执行其所指对象/F类对象的finalize()方法;
  • 当JVM判断F类对象的finalize()方法执行已/可结束后,终结者线程会断开终结者与其的关联,令两者可便于被GC回收(不是可被回收,即使不断开关联也是可以回收的)。

辅助终结

一方面为了避免终结者/F类对象累积过多而导致OOM,另一方面为了在JVM/程序退出时能尽可能保证所有F类对象的finalize()方法都被执行,终结机制存在加速/退出两种辅助终结操作(名字我自己瞎取的),会分别在上述两种情况下执行。虽说命名上有所差异,但实际上两种辅助终结操作的核心是一致的,即创建新的线程参与活动,以加速原本只由终结者线程进行的终结机制的执行效率。但其中需要注意的是:创建的辅助线程并非终结者线程类的实例,只是普通的用户线程,这些线程在名义上被称为二级终结者线程。

  • 当终结者/F类对象累积过多时,为了避免出现OOM,会触发加速辅助终结操作。
  • 当JVM/程序退出时,为了尽可能保证所有F类对象的finalize()方法都被执行,会触发退出辅助终结操作。
  • F类对象的finalize()方法只能被终结机制执行一次。
  • F类对象至少要两次GC才能被真正回收。

JVM参数

-XX:+RegisterFinalizersAtInit:设置终结者加入未终结链表的时间。为false时在F类对象实例化后、初始化前加入,为true时在F类对象初始化后加入。默认true。

Cleaner

本文讲述的是JDK8中位于sun.misc包下的Cleaner(清洁工)类(高版本中被迁移至jdk.internal.ref包)。而从JDK9开始,Java在java.lang.ref包下实现了另一个清洁工类,且该类并非PhantomReference(虚引用)类的子类。虽说作用高度相似,但实际上两者却各自发挥着作用,因此请注意区别两者。由于新清洁工类自身携带有一套完整运行流程的缘故,因此目前的主流说法中都将JDK9版本清洁工类的运行流程称之为清洁工机制,故本文中不会出现清洁工机制这个称呼。

洁工类确实实现了与终结机制/finalize()方法相同的功能,即在所指对象被GC回收时执行自定义操作。与常规注册了引用队列的Reference(引用)抽象类对象不同,拿WeakReference(弱引用)类对象举例,如果我们想在其所指对象被GC回收时执行一些操作,首先需要等待引用机制将弱引用置入引用队列中,随后再将之从中取出后或执行弱引用的isEnqueued()方法,因为我们需要通过该操作来判断所指对象是否已/会被GC回收。换句话说就是我们需要先手动的判断所指对象是否已/会被GC回收再去执行自定义操作…这就增加了我们编码的复杂性,但如果使用清洁工的话我们就不需要再做这一步了。
清洁工类继承自虚引用类,这意味着其本身也是一个虚引用。当清洁工的所指对象被GC回收时,按照引用机制的统一流程,其会被置入引用队列中。但之前在引用抽象类的文章中已经特意提及过,引用机制在将引用加入引用队列前存在一个特殊判断,即如果引用机制发现引用是一个清洁工,则会执行其内部包含的自定义操作。这意味着我们无需再做手动的判断,甚至于自定义操作都不会发生在用户线程中,引用机制会直接在后台自动处理执行自定义逻辑,从而简化了开发者编码。
并且不同于其他子类引用,清洁工类会在完成清洁方法后结束工作,并不会加入到引用队列中。因此它更像是我们在代码外代替Finalizer类。

字段

static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();队列
用于填充创建虚引用时必要的引用队列参数,因此所有清洁工的注册引用队列都相同,该参数没有实际作用。

static Cleaner first = null;队列头
持有清洁工链表头清洁工的引用。

Cleaner next = null;下一个
持有当前清洁工后继清洁工的引用。

Cleaner prev = null;上一个
持有当前清洁工前驱清洁工的引用。

final Runnable thunk;线程任务
保存当前清洁工需要执行的可运行/任务。

方法

Cleaner
下面方法主要运行了Reference的构造方法,并保存了待运行的任务。

private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}

add
使用头插法来新增数据到清洁工链表中。

private static synchronized Cleaner add(Cleaner var0) {
if (first != null) {
var0.next = first;
first.prev = var0;
}

first = var0;
return var0;
}

remove
将指定清洁工对象从链表中移除

private static synchronized boolean remove(Cleaner var0) {
if (var0.next == var0) {
return false;
} else {
if (first == var0) {
if (var0.next != null) {
first = var0.next;
} else {
first = var0.prev;
}
}

if (var0.next != null) {
var0.next.prev = var0.prev;
}

if (var0.prev != null) {
var0.prev.next = var0.next;
}

var0.next = var0;
var0.prev = var0;
return true;
}
}

create
通过所指对象及可运行/任务创建清洁工,创建的清洁工默认处于清洁工链表中

public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}

clean
从链表中移除,并执行可运行/任务。

public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}

System.exit(1);
return null;
}
});
}

}
}

不推荐使用清洁工。说了这么多,结果不让用,感觉有点浪费感情…但是实际上我们确实不推荐使用清洁工,因为其效果会好一些,但基本上终结机制/finalize()方法有的问题它都有。如果真的要说好处,大概就是性能好了些(终结机制/finalize()方法的执行建立在完整的引用机制流程基础上,而清洁工类则直接从中截断,性能自然会好些)和处理速度快了些(引用机制的引用处理器线程为最高优先级10,大于终结机制/finalize()方法的执行线程8的优先级,因此能获得更多的CPU资源)

总结

github

  1. 发现列表是由GC维护的引用列表,关于他的资料很少,源码中仅有几句描述,目前唯一知道的是发现列表会保存所有引用。发现列表中的引用为活跃状态,由于活跃状态是引用的初始状态,并且初始状态下其必然持有发现列表中的后继引用,因此可知引用在被创建时就被加入发现列表。发现列表中的引用会收到GC的特殊处理。
  2. GC线程会将发现列表中所指对象已被GC回收的引用移除,并半段其是否注册引用列表。否则直接抛出,变为怠惰状态;是则以头插法将引用加入待定列表。待定列表是引用加入引用队列前的临时存放点,此时引用为待定状态。
  3. 引用处理器线程(唯一)不断循环的待定列表的头部取出引用,如果引用是清洁工则执行自定义操作(清洁工)后抛出,引用变为怠惰状态,这是一个特殊判断;如果不是则将引用以头插法加入各自注册的引用队列中,此时引用为入队状态。
  4. Java线程从引用队列头部取出引用,以确定引用的所指对象已被GC回收,并以此为契机执行自定义操作,此时引用变为怠惰状态。
  5. f-queue是由终结者类内部创建的全局唯一引用队列,所有的终结者都会被加入该引用队列中。
  6. 终结者线程会持续将终结者从f-queue头部出队,并将其从未终结链表中移除,随后执行其所指对象的finalize()方法。finalize()方法执行结束后,终结者与其所指对象的才会断开,这与JDK8之前的虚引用很相似,但与其他引用不同。
  7. 当F类对象创建时,JVM会自动将之作为所指对象创建终结者。终结者除发现列表外还会被加入到未终结列表中。未终结列表是一个全局唯一的双向链表,作用是持有终结者的引用以防止被GC回收。
  8. 清洁工除发现列表外还会加入到清洁工链表。清洁工链表是一个全局唯一的双向链表,作用是持有清洁工的引用以防止被GC回收。

建议

在最后,这两个都不建议使用,因为作为处理关闭或析构功能,他的可靠性、效率性都非常低,使用他们更建议使用try-catch-finally来完成。并且他们对异常也相对敏感,需要自行完成。因此我们更建议将这两个功能作为一个保底方案,即最后的安全保障。
第二是在使用unsafe来完成jni层面上的内存管理时,来使用cleaner来完成数据的清空工作是一个很好的内存控制方案。因为该内存已经脱离了jvm的控制,因此使用Cleaner来管理。