前言
文章
- 相关系列:《Java ~ Reference【目录】》(持续更新)
- 相关系列:《Java ~ Reference ~ Cleaner【源码】》(学习过程/多有漏误/仅作参考/不再更新)
- 相关系列:《Java ~ Reference ~ Cleaner【总结】》(学习总结/最新最准/持续更新)
- 相关系列:《Java ~ Reference ~ Cleaner【问题】》(学习解答/持续更新)
一 概述
简介
需要提前说明的是,本文讲述的是JDK8中位于sun.misc包下的Cleaner(清洁工)类(高版本中被迁移至jdk.internal.ref包)。而从JDK9开始,Java在java.lang.ref包下实现了另一个清洁工类,且该类并非PhantomReference(虚引用)类的子类。虽说两者作用高度相似(应该是对旧功能的新实现),但实际上两者都被保留下来并各自发挥着作用,因此请注意区别两者。由于新清洁工类自身携带有一套完整运行流程的缘故,因此目前的主流说法中都将JDK9版本的清洁工类的运行流程称之为清洁工机制,故本文中不会出现清洁工机制这个称呼。
清洁工类是专门为了替代Finalization(终结)机制/finalize()方法而实现的,学习者很容易冒出这个想法…该想法是否正确暂且不论,但清洁工类确实实现了与终结机制/finalize()方法相同的功能,即在所指对象被GC回收时执行自定义操作。与常规注册了引用队列的Reference(引用)抽象类对象不同,拿WeakReference(弱引用)类对象举例,如果我们想在其所指对象被GC回收时执行一些操作,首先需要等待引用机制将弱引用置入引用队列中,随后再将之从中取出后或执行弱引用的isEnqueued()方法,因为我们需要通过该操作来判断所指对象是否已/会被GC回收。换句话说就是我们需要先手动的判断所指对象是否已/会被GC回收再去执行自定义操作…这就增加了我们编码的复杂性,但如果使用清洁工的话我们就不需要再做这一步了。
清洁工类继承自虚引用类,这意味着其本身也是一个虚引用。当清洁工的所指对象被GC回收时,按照引用机制的统一流程,其会被置入引用队列中。但之前在引用抽象类的文章中已经特意提及过,引用机制在将引用加入引用队列前存在一个特殊判断,即如果引用机制发现引用是一个清洁工,则会执行其内部包含的自定义操作。这意味着我们无需再做手动的判断,甚至于自定义操作都不会发生在用户线程中,引用机制会直接在后台自动处理执行自定义逻辑,从而简化了开发者编码。
事实上上述做法是不规范的,虽然这看起来只是单纯的特殊判断,但本质却是上级耦合了下级,在父类中对子类做特殊处理并不合常规…因此会有清洁工类是专门为了替代终结机制/finalize()方法而实现的这种想法也就理所当然了。除此之外还有一点也能应征这个想法,即清洁工在执行完自定义操作后会放弃加入引用队列,并直接退出流程开始对下个引用做处理…连引用队列都不入了…说它不是专门为了取代终结机制/finalize()方法谁信呐?
与终结机制/finalize()方法的对比
不推荐使用清洁工。说了这么多,结果不让用,感觉有点浪费感情…但是实际上我们确实不推荐使用清洁工,因为其效果会好一些,但基本上终结机制/finalize()方法有的问题它都有。如果真的要说好处,大概就是性能好了些(终结机制/finalize()方法的执行建立在完整的引用机制流程基础上,而清洁工类则直接从中截断,性能自然会好些)和处理速度快了些(引用机制的引用处理器线程为最高优先级10,大于终结机制/finalize()方法的执行线程8的优先级,因此能获得更多的CPU资源),但总体来说还是不推荐使用的…程序哪天崩了都不知道。
二 使用
创建
清洁工类没有公共的构造方法,具体的实例通过其提供的工厂方法创建,该设计与清洁工的生命周期有关。
- public static Cleaner create(Object var0, Runnable var1) —— 创建 —— 创建指定所指对象及自定义操作的清洁工,该清洁工会被默认以头插法加入清洁工链表中。
方法首先会判断传入的自定义操作是否为null,是则直接返回null,因为一个没有指定自定义操作的清洁工是没有意义的;否则调用私有Cleaner(Object var1, Runnable var2)构造方法创建指定所指对象及自定义操作的清洁工。清洁工作为虚引用理论上必须搭配引用队列使用,但由于清洁工会收到引用机制的特殊处理,即其并不会被加入引用队列中,因此在创建清洁工时无需手动指定引用队列,清洁工类会使用自带的全局静态常量“假”引用队列来默认填充虚引用类的构造方法参数。
清洁工被成功创建后,方法会自动以头插法将之加入清洁工链表中,这也是清洁工类只提供工厂方法而不提供公共构造方法的根本原因,其目的是确保每个被创建的清洁工都默认存在于清洁工链表里。清洁工链表的作用是保证清洁工无论在什么环境下被使用,都不会在自定义操作被引用机制执行前被GC回收,该知识点会在下文详述。
public static Cleaner create(Object var0, Runnable var1) {
// 实例化一个清洁工,并将之插入到清洁工链表中。
return var1 == null ? null : add(new Cleaner(var0, var1));
}
private Cleaner(Object var1, Runnable var2) {
// 调用虚引用类的构造方法。清洁工的本质是一个虚引用类对象,因此必然存在一个所指对象。
super(var1, dummyQueue);
// 设置自定义操作。
this.thunk = var2;
}
方法
- public void clean() —— 清理 —— 将当前清洁工从清洁工链表中移除并执行自定义操作。该方法只在首次调用时有效,即只会在首次调用时执行自定义操作,后续调用不会再执行。该方法会在引用机制中被调用。
三 实现
清洁工链表
清洁工链表的核心作用是防止清洁工在被引用机制执行自定义操作前被GC回收。我们并无法限定清洁工在实际使用中的场景,也就是说无论开发者是有意还是无意,清洁工都可能处于一个完全没有与GC ROOTS建立直接/间接关联的场景中,如此造成的后果就是清洁工完全可能在被引用机制执行自定义操作前被GC回收(更准确的说是加入待定列表前,因为待定链表也是全局静态变量,属于GC ROOTS的范畴,但发现列表不是)。这个后果是不可以被接受的,因为清洁工并不像纯粹的软/弱/虚引用一样只是单纯的影响所指对象的GC,其本身携带的自定义操作也注定了它属于所指对象运行逻辑的一部分,因此为了保证清洁工的自定义操作一定会被执行,就必须人为将清洁工与GC ROOTS建立直接/间接关联,而清洁工链表起的就是这个作用。
清洁工链表是由清洁工类内部持有的全局唯一双向链表,即清洁工链表是一个全局静态变量。清洁工链表是逻辑链表,即清洁工链表并不是一个类似于LinkedList(链接列表)类对象的对象。清洁工类中用于持有清洁工链表的[first @ 首个]实际上持有的只是清洁工链表的头清洁工/节点,由于每个清洁工都持有其前驱/后继清洁工/节点,因此只要持有了头清洁工/节点就相当于持有了整个清洁工链表。
当清洁工被创建时,其会被默认以头插法加入清洁工链表中,这也是清洁工类不提供公共构造方法的根本原因之一,而另一个根本原因是避免创建未指定自定义操作的清洁工。由于每个清洁工在创建时就会被默认加入清洁工链表中,因此所有清洁工与GC ROOTS之间都至少会存在一条可达链路,故而无论清洁工在具体的使用场景中是否与GC ROOTS建立了其它直接/间接关联,都保证了其在被引用机制执行自定义操作之前不会被GC回收。而当执行自定义操作时,即引用机制调用清洁工的clean()方法时,clean()方法会先将清洁工从清洁工链表中移除后再执行自定义操作,目的是断开额外可达链路以防止在自定义操作执行后受其影响而无法被GC回收(虽然额外链路被断开,但此时清洁工还被作为引用处理器线程的局部变量,因此无需担心自定义操作执行期间清洁工被GC回收)。
clean()方法只有在首次调用时有效。关于这一点在上文中已经提及过,由于clean()方法是公共方法,因此其也可能被开发者调用,在开发者已手动调用clean()方法的情况下,引用机制调用clean()方法也不会再重复执行自定义操作(反之亦然),这是因为将清洁工从清洁工链表移除是执行自定义操作的先决条件,而一个清洁工无法被移除多次(但凭心而论自定义操作只能被执行一次应该是设计上的问题,否则完全可以将清洁工移除和自定义操作执行拆分为两步操作,或者无论是否移除都可自由执行)。由于所有清洁工会共用同一个清洁工链表,因此清洁工类的插入/移除静态方法被修饰了synchronized关键字实现类锁以保证线程安全。清洁工的移除无需遍历清洁工链表,因为清洁工本身持有前驱/后继清洁工/节点,因此可以直接进行链接。当然,如果是头清洁工/节点则需要特殊判断以更新[首个]。清洁工被移除后会将之前驱/后继清洁工/节点设置为自身,即自引用,作为其已被移除的判断依据,clean()方法就是通过该依据判断清洁工是否已被移除/方法是否已被执行的(之所以不使用null是因为null已经被作为头/尾清洁工/节点的标志)。
四 使用案例
清洁工类有一个较为著名的使用案例,即堆外内存的回收,而要阐述清楚这一点,就必须了解DirectByteBuffer(直接比特缓存)类。直接比特缓存类与堆外内存的管理有关,在此不做讨论,只需知道其用于分配/回收堆外内存即可。直接比特缓存虽然只是普通的Java对象,却可能关联着一块非常大的堆外内存,因此直接比特缓存被GC回收前必须释放/回收这些堆外内存,否则这些堆外内存会一直被Java进程持有而无法重新分配,因为众所周知,GC在正常情况下是无法释放/回收堆外内存的(也有不正常的情况,当堆内存溢出的时会触发Full GC顺便一起回收)。
为了保证堆外内存一定被释放,直接比特缓存类使用清洁工类实现了一个保底机制,即当直接比特缓存被GC回收时,会触发清洁工对堆外内存进行回收。在直接比特缓存类中组合了一个清洁工类字段,该字段会在直接比特缓存初始化时被赋值为清洁工,该清洁工以当前直接比特缓存作为所指对象,源码如下:
private final Cleaner cleaner;
// Primary constructor
//
DirectByteBuffer(int cap) { // package-private
// 此处忽略。
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 创建清洁工。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper) {
super(-1, 0, cap, cap, fd);
address = addr;
// 创建清洁工。
cleaner = Cleaner.create(this, unmapper);
att = null;
}
在上述代码中,创建清洁工时第二个参数传入了一个Deallocator(释放者)类对象。释放者类是直接比特缓存类自实现的实现了Runnable(可运行)接口的静态内部类,其在run()方法中编码了堆外内存的释放逻辑。源码如下:
/**
* @Description: 释放者/器类(看不懂没关系,知道是释放堆外内存的就行)
*/
private static class Deallocator implements Runnable {
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
/**
* @Description: 释放堆外内存
*/
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 释放对外内存。
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
当直接比特缓存被GC回收时,清洁工也会因为引用而执行入队操作,从而引发对清洁工的特殊处理。清洁工不会入队,而是会执行其内部的自定义操作,也就是释放者的run()方法,从而完成堆外内存的释放。