Finalize被废弃,Native垃圾回收该怎么办?
前言
众所周知Finalize机制存在各种缺陷,因此在Java9中,该机制最终被废弃,取而代之的是java.lang.ref.Cleaner
,Cleaner相对于Finalize更加轻量、健壮。
在座的您可能坐不住了: “这是Java9的东西,我大Android连Java8的东西都用不全,哪里顾得上Java9的东西?”
别急,且听我慢慢道来。这Cleaner其实很早就存在于JDK之中了,之前一直被JDK内部偷偷的使用,藏着掖着的所以大伙都不怎么熟悉。我们的Android也早在8.0中使用它实现了NativeAllocationRegistry
,用来做Native内存的回收工作,比如bitmap本地像素数据的释放。
在Java9里,Cleaner被重构了一番后公开出来了,这个版本的实现则是PhantomReference
良好使用范例,相信看完本文后你也可以依葫芦画瓢弄出属于自己的Native Source Cleaner。
简单使用
Cleaner最开始是JDK内部提供的API,在sun.misc
包下。从Java9开始,该类被重构了,新的实现为java.lang.ref.Cleaner
,原有的Cleaner实现则被移动到了jdk.internal.ref.Cleaner
。
sun.misc.Cleaner
要使用sun.misc.Cleaner
,首先需要将清理代码包装成一个Runnable对象:
public class CleanNativeTask implements Runnable {
private long ptr = 0;
public CleanNativeTask(long ptr) {
this.ptr = ptr;
}
@Override
public void run() {
System.out.println("runing CleanNativeTask");
if (address != 0) {
GetUsafeInstance.getUnsafeInstance().freeMemory(ptr);
}
}
}
然后构造一个Cleaner对象,将需要监控的Java对象与承载清理代码的Runnable对象关联起来。当Java对象被GC回收时,Runnable中的代码就会自动执行。
public class ObjectInHeapUseCleaner {
private long ptr = 0;
public ObjectInHeapUseCleaner() {
ptr = GetUsafeInstance.getUnsafeInstance().allocateMemory(2 * 1024 * 1024);
}
public static void main(String[] args) {
while (true) {
System.gc();
ObjectInHeapUseCleaner heap = new ObjectInHeapUseCleaner();
Cleaner.create(heap, new CleanNativeTask(heap.ptr));
}
}
}
清理动作的Runnable尽量不要以非静态内部类/lambda的形式实现,因为非静态内部类容易持有外部类的引用,使得监控对象无法成为虚引用可达对象(phantom reachable),影响清理动作的执行。
java.lang.ref.Cleaner
java.lang.ref.Cleaner
在使用前需要先创建一个Cleaner对象,在创建Cleaner对象时可以为其传递一个ThreadFactory
对象,用以指定清理代码的执行线程。同样的,清理代码也需要用Runnable包装。然后调用cleaner对象的register方法将目标对象与清理动作关联起来。
public class CleaningExample {
private static final Cleaner cleaner = Cleaner.create();
public CleaningExample() {
ObjectInHeapUseCleaner heap = new ObjectInHeapUseCleaner();
this.cleanable = cleaner.register(this, new FreeMemoryTask(heap.ptr));
}
}
基本原理
虚引用 PhantomReference
Cleaner用于在Java对象被GC时同步回收对应的Native内存,因此需要追踪Java对象的回收时机(以下用referent
指代这个被追踪的Java对象)。Cleaner使用虚引用(PhantomReference
)来跟踪referent
的生命周期。下图展示了虚引用的处理过程:
在GC过程中,referent
被强引用持有时不会被回收。当referent
仅剩下虚引用持有时,就可以被GC回收,同时虚引用会被加入到指定的ReferenceQueue中,等待后续处理。
sun.misc.Cleaner的实现
Reference的处理在ART和JVM上是不一样的,因此sun.misc.Cleaner在这两个平台上的实现也略有不同,但是设计思路是一样的。文中给出的JVM平台的代码基于JDK12,此时sun.misc.Cleaner已被移动到
jdk.internal.ref
包下。
Cleaner直接继承自PhantomReference
,并且提供了一个clean方法用于间接的调用清理代码。
// ART: [libcore/ojluni/src/main/java/sun/misc/Cleaner.java](https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/sun/misc/Cleaner.java)
// JDK: jdk/internal/ref/Cleaner.java
public class Cleaner
extends PhantomReference<Object>
{
...
private final Runnable thunk; // 包装清理代码的Runnable
private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue);
this.thunk = thunk;
}
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}
...
public void clean() {
if (!remove(this))
return;
try {
thunk.run();
} catch (final Throwable x) {
...
}
}
}
我们为referent
创建了Cleaner后,也就相当于为其创建了一个虚引用。因此,当referent
对象被GC后,cleaner能得到感知,与PhantomReference
不同的地方是,cleaner不会被加入到ReferenceQueue中,它在入队的过程中就会得到执行。
// ART: [libcore/ojluni/src/main/java/java/lang/ref/ReferenceQueue.java](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:libcore/ojluni/src/main/java/java/lang/ref/ReferenceQueue.java;l=77;drc=android-11.0.0_r1)
public class ReferenceQueue<T> {
...
private boolean enqueueLocked(Reference<? extends T> r) {
if (r.queueNext != null) {
return false;
}
if (r instanceof Cleaner) {
// 如果要入队的Reference是Cleaner,则直接调用其clean方法。
Cleaner cl = (sun.misc.Cleaner) r;
cl.clean();
r.queueNext = sQueueNextUnenqueued;
return true;
}
...
}
...
}
// JDK12: java/lang/ref/Reference.java
private static void processPendingReferences() {
waitForReferencePendingList();
Reference<Object> pendingList;
synchronized (processPendingLock) {
pendingList = getAndClearReferencePendingList();
processPendingActive = true;
}
while (pendingList != null) {
Reference<Object> ref = pendingList;
pendingList = ref.discovered;
ref.discovered = null;
if (ref instanceof Cleaner) {
((Cleaner)ref).clean();
...
} else {
ReferenceQueue<? super Object> q = ref.queue;
if (q != ReferenceQueue.NULL) q.enqueue(ref);
}
}
...
}
可以看到,Cleaner在ReferenceQueue的处理过程中被当作是一种特殊的对象,clean方法在入队之前就得到调用。因此在使用sun.misc.Cleaner
时,需要保证clean方法中的是快速的,以防阻塞其它Reference的处理。
java.lang.ref.Cleaner的实现
在java.lang.ref.Cleaner
中,Cleaner不再直接继承PhantomReference
以实现referent对象的跟踪与清理,取而代之的是Cleanable
,它将替代Cleaner成为PhantomReference
的子类。而新的cleaner对象则是一个管理者,它管理所有通过它注册的Cleanable对象,并负责在指定的线程上执行它们的清理代码。
// JDK: java/lang/ref/Cleaner.java
public final class Cleaner {
...
public static Cleaner create() {
Cleaner cleaner = new Cleaner();
cleaner.impl.start(cleaner, null);
return cleaner;
}
// threadFactory用于指定清理代码的执行线程
public static Cleaner create(ThreadFactory threadFactory) {
Cleaner cleaner = new Cleaner();
cleaner.impl.start(cleaner, threadFactory);
return cleaner;
}
// 注册被监控的对象以及对应的清理代码
public Cleanable register(Object obj, Runnable action) {
return new CleanerImpl.PhantomCleanableRef(obj, this, action);
}
}
与sun.misc.Cleaner
不同的是,Cleanable在ReferenceQueue的处理过程不会被特殊对待,它像其它引用一样进入指定的ReferenceQueue。这个引用队列在CleanerImpl
中创建,CleanerImpl
则作为Cleaner的成员随之一起创建。
// JDK: java/lang/ref/Cleaner.java
public final class Cleaner {
final CleanerImpl impl;
private Cleaner() {
impl = new CleanerImpl();
}
....
}
// JDK: jdk/internal/ref/CleanerImpl.java
public final class CleanerImpl implements Runnable {
final ReferenceQueue<Object> queue;
public CleanerImpl() {
queue = new ReferenceQueue<>();
...
}
...
}
随后,在使用Cleaner.register方法创建Cleanable对象时,CleanerImpl
中的ReferenceQueue将用于初始化PhantomReference
,也就前面提到的referent被回收时虚引用要进入的ReferenceQueue。
// JDK: jdk/internal/ref/PhantomCleanable.java
public abstract class PhantomCleanable<T> extends PhantomReference<T>
implements Cleaner.Cleanable {
public PhantomCleanable(T referent, Cleaner cleaner) {
super(Objects.requireNonNull(referent), CleanerImpl.getCleanerImpl(cleaner).queue);
this.list = CleanerImpl.getCleanerImpl(cleaner).phantomCleanableList;
insert();
...
}
}
当Cleanable进入到这个队列时,就表明对应的referent已经被GC了。此时,在CleanerImpl中的清理线程将从中获取到Cleanable对象,然后主动调用它的clean方法,进而间接的执行到runnable中封装的清理代码。
// JDK: jdk/internal/ref/CleanerImpl.java
public final class CleanerImpl implements Runnable {
...
// 启动清理线程,在Cleaner对象被创建时执行
public void start(Cleaner cleaner, ThreadFactory threadFactory) {
...
if (threadFactory == null) {
threadFactory = CleanerImpl.InnocuousThreadFactory.factory();
}
Thread thread = threadFactory.newThread(this);
thread.setDaemon(true);
thread.start();
}
public void run() {
...
while (!phantomCleanableList.isListEmpty() ||
!weakCleanableList.isListEmpty() ||
!softCleanableList.isListEmpty()) {
...
try {
Cleanable ref = (Cleanable) queue.remove(60 * 1000L);
if (ref != null) {
ref.clean();
}
} catch (Throwable e) {
...
}
}
}
}
java.lang.ref.Cleaner
利用PhantomReference
的ReferenceQueue,将清理代码的执行从GC过程中分离了出来,可以避免因使用不当而影响整个引用处理过程,同时也让开发者可以灵活的指定清理代码的执行线程。相对于sun.misc.Cleaner
会更加灵活、安全一点。
对比Finalize
- Cleaner使用虚引用跟踪目标引用的生命周期,清理代码也从目标引用中独立出来,因此不会影响目标引用的GC。
- Finalize中抛出
unchecked exceptions
是无法感知的(不会通过日志打印异常栈),Cleaner中则不存在此问题。 - FinalizerThread的优先级通常低于应用程序线程,如果被回收对象的出队速度低于入队速度,就会引发OOM。Cleaner则可以自己控制Cleaner机制的线程,相比Finalize稍微好一点。
参考
转载自:https://juejin.cn/post/6921910427408400392