蹲厕所的熊

benjaminwhx

避免使用Finalize方法

2018-05-17 作者: 吴海旭


  1. 概述
  2. Object中的finalize方法
  3. 注册f类
  4. 调用finalize方法
  5. finalize方法导致内存溢出
  6. 结论
  7. 参考

概述

  1. 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。是使用终结方法会导致行为不稳定、降低性能,以及可移植性问题。所以,我们应该避免使用终结方法。
  2. 使用终结方法有一个非常严重的性能损失。在我的机器上,创建和销毁一个简单对象的时间大约为5.6ns、增加一个终结方法使时间增加到了2400ns。换句话说,用终结方法创建和销毁对象慢了大约430倍。
  3. 如果实在要实现终结方法,要记得调用super.finalize()。

上面的3点是出自Effective Java第二版第七条中的部分内容,可能刚开始我们看的时候一脸懵逼。有的人甚至都没听过finalize方法,更不知道用了它会出现什么问题了。下面我们来说说finalize方法。

Object中的finalize方法

protected void finalize() throws Throwable { }

我们可以看到Object中的finalize方法什么都没有实现,而且修饰符是protected,明显可以看出来是由子类去实现它的。这个方法的原意是在GC发生时销毁一些资源使用的,那么什么时候会调用这个方法呢?

原来在类加载的时候,会去检查一个类是否含有一个参数为空,返回值为void的finalize方法,还要求finalize方法必须非空。这个类我们暂时称为f类

注册f类

比如我们有一个类A,它重写了finalize方法,在new A()的时候首先标记它是一个f类,然后调用Object的空构造方法,这个地方hotspot在初始化Object的时候将return指令替换为_return_register_finalizer指令,该指令并不是标准的字节码指令,是hotspot扩展的指令,这样在处理该指令时调用Finalizer.register方法,以很小的侵入性代价完美地解决了这个问题。下面是register的源码。

final class Finalizer extends FinalReference<Object> { 
    // 引用队列
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    // 静态的Finalizer链
    private static Finalizer unfinalized = null;
    private static final Object lock = new Object();

    private Finalizer
        next = null,
        prev = null;

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

    /**
     * unfinalized链不为空,让自己指向unfinalized,unfinalized的prev指向自己
     * unfinalized指向自己
     * 最终unfinalized将指向最后加进来的对象,并且这个链包含所有实现finalize方法的对象
     */
    private void add() {
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }

    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add();
    }

    /* Invoked by VM */
    // 这个register就是在new Object()的时候进行调用的
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
}

通过源码我们可以知道register除了把实现finalize方法的对象加到一个名为unfinalized的链表中外,还在构造方法中调用了super(finalizee, queue);,最终进入了Reference的构造方法中。

class FinalReference<T> extends Reference<T> {

    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

public abstract class Reference<T> {

    // 用于保存对象的引用,GC会根据不同Reference来特别对待
    private T referent;         /* Treated specially by GC */
    // 如果需要通知机制,则保存的对对应的队列
    volatile ReferenceQueue<? super T> queue;
    /* 这个用于实现一个单向循环链表,用以将保存需要由ReferenceHandler处理的引用 */
    Reference next;

    transient private Reference<T> discovered;  /* used by VM */

    static private class Lock { };
    private static Lock lock = new Lock();

    // 此属性保存一个PENDING的队列,配合上述next一起使用
    private static Reference<Object> pending = null;

    /* High-priority thread to enqueue pending References
     */
    private static class ReferenceHandler extends Thread {

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            for (;;) {
                Reference<Object> r;
                synchronized (lock) {
                    if (pending != null) {
                        // 取得当前pending的Reference链
                        r = pending;
                        // pending指向Reference链的下一个元素discovered
                        pending = r.discovered;
                        r.discovered = null;
                    } else {
                        try {
                            try {
                                lock.wait();
                            } catch (OutOfMemoryError x) { }
                        } catch (InterruptedException x) { }
                        continue;
                    }
                }

                // Fast path for cleaners
                if (r instanceof Cleaner) {
                    ((Cleaner)r).clean();
                    continue;
                }

                ReferenceQueue<Object> q = r.queue;
                // 入队列
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }
    }

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

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

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

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

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

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

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

}

Reference中有两个变量pending和discovered,我们它们两个没有地方可以赋值,都是由GC来操作的,下面是状态图:

Reference内部有一个线程ReferenceHandler,一旦使用了Reference,则会启动该线程。该线程会拿到pending的Reference,把它加入到ReferenceQueue中。并把queue的状态设为ENQUEUED,并通过Reference的next属性把对象串起来,犹如一个链表。下面是ReferenceQueue的enqueue()

boolean enqueue(Reference<? extends T> r) {
    synchronized (lock) {
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        r.queue = ENQUEUED;
        // r的next节点指向当前头结点
        r.next = (head == null) ? r : head;
        // 头结点指向当前对象r
        head = r;
        queueLength++;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1);
        }
        lock.notifyAll();
        return true;
    }
}

调用finalize方法

我们在回到Finalizer类中,我们发现它里面也有一个内部线程,会先从queue中取出之前初始化对象时放进去的对象,在调用runFinalizer方法,这个方法主要就是调用对象的finalize方法,接着把对象置空,等待下一次gc清除对象。

private void runFinalizer(JavaLangAccess jla) {
    synchronized (this) {
        if (hasBeenFinalized()) return;
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            // 调用对象的finalize方法
            jla.invokeFinalize(finalizee);
            finalizee = null;
        }
    } catch (Throwable x) { }
    super.clear();
}

private static class FinalizerThread extends Thread {
    private volatile boolean running;
    FinalizerThread(ThreadGroup g) {
        super(g, "Finalizer");
    }
    public void run() {

        // ...

        for (;;) {
            try {
                // 从queue中取出之前初始化放进去的元素
                Finalizer f = (Finalizer)queue.remove();
                f.runFinalizer(jla);
            } catch (InterruptedException x) {
                // ignore and continue
            }
        }
    }
}

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread finalizer = new FinalizerThread(tg);
    finalizer.setPriority(Thread.MAX_PRIORITY - 2);
    finalizer.setDaemon(true);
    finalizer.start();
}

finalize方法导致内存溢出

网上很多文章讲的很明白了:

Java的Finalizer引发的内存溢出
重载Finalize引发的内存泄露

主要原因是:Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐。所以最后会发生OutOfMemoryError异常。

结论

C++有析构函数这个东西,能够很好地在对象销毁前做一些释放外部资源的工作,但是java没有。Object.finalize()提供了与析构函数类似的机制,但是它不安全、会导致严重的内存消耗和性能降低,应该避免使用。best practice是:像java类库的IO流、数据库连接、socket一样,提供显示的资源释放接口,程序员使用完这些资源后,必须要显示释放。所以可以忘记Object.finalize()的存在。JVM启动的时候,会创建一个Finalizer线程来支持finalize方法的执行。关于引用和引用队列,java提供了4种引用类型,在垃圾回收的时候,都有自己各自的独特表现。ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,这相当于是一种通知机制。当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。JVM提供了一个ReferenceHandler线程,将引用加入到注册的引用队列中。

finalze机制是先执行Object.finalize()中的逻辑,后销毁堆中的对象;引用和队列机制,先销毁对象,后执行我们自己的逻辑。可以看到:使用引用和队列机制效率更高,因为垃圾对象释放的速度更快。如果是监控对象的销毁,那么最适合的是幽灵引用,如sun.misc.Cleaner就是使用幽灵引用,达到监控对象销毁的目的,NIO中使用的就是这个。

参考

Java Reference 源码分析
JVM源码分析之FinalReference完全解读-你假笨
话说ReferenceQueue
finalize、Finalizer和Finalizer Queue的原理
InfoQ-Java将弃用finalize()方法?



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论