Finalize被废弃,Native垃圾回收该怎么办?

2,777 阅读6分钟

前言

众所周知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

  1. Cleaner使用虚引用跟踪目标引用的生命周期,清理代码也从目标引用中独立出来,因此不会影响目标引用的GC。
  2. Finalize中抛出unchecked exceptions是无法感知的(不会通过日志打印异常栈),Cleaner中则不存在此问题。
  3. FinalizerThread的优先级通常低于应用程序线程,如果被回收对象的出队速度低于入队速度,就会引发OOM。Cleaner则可以自己控制Cleaner机制的线程,相比Finalize稍微好一点。

参考