PhantomReference & Cleaner

在讲DirectBuffer的时候,圈里有同学问我,关于DirectBuffer如何回收的问题。我当时回答说别急,等我讲完GC再来讲这个问题。课程进行到现在,终于可以讲一下了。

其实,在讲DirectBuffer的时候,就已经贴过这一段代码了,但是当时没有做为重点去讲解。今天我们再返回来看一下DirectByteBuffer的构造函数:

   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就是DirectBuffer回收的最关键部分。
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

前边的部分大家再复习一下,今天的重点是倒数第三行的那个cleaner到底是什么鬼。我们点开Cleaner的定义看一下:

public class Cleaner
    extends PhantomReference<Object>
{

    // Dummy reference queue, needed because the PhantomReference constructor
    // insists that we pass a queue.  Nothing will ever be placed on this queue
    // since the reference handler invokes cleaners explicitly.
    // 就像英文注释所说的,这货没啥卵用。后面我会讲到的。
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    // Doubly-linked list of live cleaners, which prevents the cleaners
    // themselves from being GC'd before their referents
    // 所有的cleaner都会被加到一个双向链表中去,这样做是为了保证在referent被回收之前
    // 这些Cleaner都是存活的。
    static private Cleaner first = null;

    private Cleaner
        next = null,
        prev = null;

    // 构造的时候把自己加到双向链表中去
    private static synchronized Cleaner add(Cleaner cl) {
        if (first != null) {
            cl.next = first;
            first.prev = cl;
        }
        first = cl;
        return cl;
    }

    // clean方法会调用remove把当前的cleaner从链表中删除。
    private static synchronized boolean remove(Cleaner cl) {
        // If already removed, do nothing
        if (cl.next == cl)
            return false;

        // Update list
        if (first == cl) {
            if (cl.next != null)
                first = cl.next;
            else
                first = cl.prev;
        }
        if (cl.next != null)
            cl.next.prev = cl.prev;
        if (cl.prev != null)
            cl.prev.next = cl.next;

        // Indicate removal by pointing the cleaner to itself
        cl.next = cl;
        cl.prev = cl;
        return true;
    }

    // 用户自定义的一个Runnable对象,
    private final Runnable thunk;

    // 私有有构造函数,保证了用户无法单独地使用new来创建Cleaner。
    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
    }

    /**
     * 所有的Cleaner都必须通过create方法进行创建。
     */
    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        return add(new Cleaner(ob, thunk));
    }

    /**
     * 这个方法会被Reference Handler线程调用,来清理资源。
     */
    public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
}

这段代码的关键注释我已经加上了。然后我再把要注意的点单独强调一下。

  1. Cleaner继承自PhantomReference,它本质上仍然是一个Reference。所以它的处理方法与WeakReference,SoftReference十分相似。仍然是由GC标记,Reference Handler线程处理的。
  2. Cleaner本身不带有清理逻辑,所有的逻辑都封装在thunk中,因此thunk是怎么实现的才是最关键的。
  3. Cleaner中的next和prev是private的,一定要记住这一点,不要和Reference的成员变量next 混淆了。它们不是一回事。这个next, prev是双向链表,而Reference的next则是由JVM维护的。

OK,关于Reference Handler的代码,请看这篇文章:WeakReference

注意看啊,Reference的定义里新启的那个线程,它的run方法会专门判断从pending链表上取出来的那个对象是不是Cleaner,如果是就会调用它的clean方法。所以我们知道了,Cleaner的clean方法是由Reference Handler线程调用的

好,到此为止,我们再回顾一下整个全景图:

首先,DirectBuffer buf 如果已经没有强引用了,那么JVM就会发现只有一个Cleaner还在引用着这个buf,那么就会把与此buf相关的那个cleaner放到一个名为pending的链表里。这个链表是通过Reference.discovered域连接在一起的。

接着,Reference Handler这个线程会不断地从这个pending链表上取出新的对象来。它可能是WeakReference,也可能是SoftReference,当然也可能是PhantomReference和Cleaner。

最后,如果是Cleaner,那就直接调用Cleaner的clean方法,然后就结束了。其他的情况下,要交给这个对象所关联的queue,以便于后续的处理。

关于Cleaner和PhantomReference就讲这么多了,下节课讲finalize,还会再补充一点cleaner和Finalizer的对比。如果有同学还想了解更多,就评论或者私信问我都行。当然,关于DirectBuffer,还有一点要补充,那就是Cleaner的第二个构造参数是一个Runnable类型的thunk:

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

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
    }

嗯。在它的run方法里,真正地调用了freeMemory来释放内存。

DirectBuffer释放的全部差不多就都在这里了。当然,说了这么多,其实DirectBuffer的释放时机还是不确定的。首先,得发生GC,其次,Reference Handler得调度到,然后处理到你的cleaner才行,还有一条,如果你自己要实现一个cleaner,千万千万不要在run方法里写一些执行时间很长,或者会阻塞线程的逻辑的。会把Reference Handler跑死的。

上一节课:WeakReference vs. SoftReference

下一节课:finalize方法

课程目录:课程目录

编辑于 2019-10-26

文章被以下专栏收录