避免使用finalize()方法

finalize 和 cleaner 都应该尽量避免使用

Posted by Allen Vork on November 19, 2018

Synopsis

我们知道 GC 只能回收那些使用 new 关键字分配的内存空间。如果是使用 native method 调用 C/C++ 方法 malloc() 函数系列来分配内存空间的话,必须调用 free() 函数来释放,否则这些内存空间就不会被释放,可能导致内存泄漏。因为 free() 方法是 C/C++ 中的函数,所以可以在 finalize() 中使用本地方法来调用它来释放内存。一旦 GC 准备回收内存时,它就会调用 finalize() 方法来进行一些必要的清理工作。只有到下一次 GC 回收内存时才会真正释放该对象锁占用的内存空间(后面会讲)。
finalize() 与 C++ 中的析构函数不是对应的,C++ 中的析构函数的调用时机是确定的(对象离开作用域或 delete 掉),但 java 中的 finalize() 的调用具有不确定性。

finalize 实现

final class Finalizer extends FinalReference<Object> {
    ...

    private void add() {
        Object var1 = lock;
        synchronized(lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }

            unfinalized = this;
        }
    }

    private Finalizer(Object var1) {
        super(var1, queue);
        this.add();
    }

    static void register(Object var0) { // jvm 在创建对象的时候就会调用这个方法
        new Finalizer(var0);
    }

    
    static {
        ThreadGroup var0 = Thread.currentThread().getThreadGroup();

        for(ThreadGroup var1 = var0; var1 != null; var1 = var1.getParent()) {
            var0 = var1;
        }


        // 创建一个低优先级的线程来执行 finalize 方法
        Finalizer.FinalizerThread var2 = new Finalizer.FinalizerThread(var0);
        var2.setPriority(8); // 优先级为8
        var2.setDaemon(true);
        var2.start();
    }

    private static class FinalizerThread extends Thread {
        private volatile boolean running;

        FinalizerThread(ThreadGroup var1) {
            super(var1, "Finalizer");
        }

        public void run() {
            if (!this.running) {
                ...

                while(true) {
                    while(true) {
                        try { // 这里捕获了异常,出现问题时 finalize 方法就会终止,导致对象不完整
                            // 从 ReferenceQueue 获得 finalizer 并执行 finalize 方法
                            Finalizer var2 = (Finalizer)Finalizer.queue.remove();
                            var2.runFinalizer(var1);
                        } catch (InterruptedException var4) {
                            ;
                        }
                    }
                }
            }

        }
    }

    private void runFinalizer(JavaLangAccess var1) {
        synchronized(this) {
            if (this.hasBeenFinalized()) {
                return;
            }

            this.remove();
        }

        try {
            Object var2 = this.get();
            if (var2 != null && !(var2 instanceof Enum)) {
                var1.invokeFinalize(var2);
                var2 = null;
            }
        } catch (Throwable var4) {
            ;
        }

        super.clear();
    }

    static void runFinalization() {
        if (VM.isBooted()) {
            forkSecondaryFinalizer(new Runnable() {
                private volatile boolean running;

                public void run() {
                    if (!this.running) {
                        JavaLangAccess var1 = SharedSecrets.getJavaLangAccess();
                        this.running = true;

                        while(true) {
                            Finalizer var2 = (Finalizer)Finalizer.queue.poll();
                            if (var2 == null) {
                                return;
                            }

                            var2.runFinalizer(var1);
                        }
                    }
                }
            });
        }
    }
}
  1. 在对象的初始化过程中,会判断类是否实现 finalize 并且方法体不为空,是的话,他就会调用 Finalizer#register 方法(jvm 内部执行)
  2. register 方法会将该对象添加到 Finalizer 链表头部
  3. Finalizer 类中有一个静态代码块会创建一个优先级为8的线程来从 ReferenceQueue 中获取 Finalizer 对象来执行 finalize 方法

我们知道,它是将对象放到 Finalizer 链表中,然后从 ReferenceQueue 中获取的,那么该对象是怎么放到 ReferenceQueue 中呢?

    private static class ReferenceHandler extends Thread {
        ...
        public void run() {
            while(true) {
                Reference.tryHandlePending(true);
            }
        }
        ...
    }

    // 处理 Reference 的 pending 属性,该属性其实就是 Reference 自己。
    // 它是在 GC 的时候被设置的
    static boolean tryHandlePending(boolean var0) {
        Reference var1;
        Cleaner var2;
        try {
            synchronized(lock) {
                // GC 时 jvm 会将对象设置到 Pending 中
                if (pending == null) {
                    if (var0) {
                        lock.wait();
                    }

                    return var0;
                }

                var1 = pending;
                var2 = var1 instanceof Cleaner ? (Cleaner)var1 : null;
                pending = var1.discovered;
                var1.discovered = null;
            }
        } catch (OutOfMemoryError var6) {
        }

        if (var2 != null) { // 如果 referent 是 Cleaner,直接执行 clean 即可
            var2.clean();
            return true;
        } else { // 不是的话,直接将 referent 放到 ReferenceQueue 中
            ReferenceQueue var8 = var1.queue;
            if (var8 != ReferenceQueue.NULL) {
                var8.enqueue(var1);
            }

            return true;
        }
    }

Finalizer 的构造函数中会将该对象和 ReferenceQueue 传到父类 Reference 中,它有一个高优先级的 ReferenceHandler 线程来处理这个 pending 属性,如果这个对象继承 Cleaner 则直接执行 clean 方法,不是的话,则将它放到 ReferenceQueue 中,交给 Finalizer 中的低优先级的线程来执行 finalize。
可以看出对于 Finalize ,它是在 GC 的时候在 Reference 的高优先级线程中,将对象放到 ReferenceQueue 中,由 Finalizer 类中的低优先级线程来执行 finalize 方法,而该对象如果实现了 Cleaner 的话,则是直接在 Reference 的高优先级线程中执行 clean 方法。

小结

对象创建时,jvm 会判断有没有实现 finalize,有的话就将它封装成 Finalizer 对象,当 GC 的时候,该对象会被设置到 Reference 的静态属性 pending 中,然后 Reference 内部的线程会将他放到 ReferenceQueue 中,由 Finalizer 类中的低优先级线程来执行 finalize 方法。

Finalize Issues

从上面的实现就可以看出它有以下缺陷:

  • 由于它是在低优先级线程中执行的,而且是在对象被回收前执行,所以它可能导致独享经过多个垃圾收集周期才会被回收,这可能导致大量对象堆积而出现 OOM。
  • java 语言规范并不保证 finalize 方法会被及时地执行,而且根本不保证他们会执行。(因为如果内存充足的话,垃圾回收是不会执行的,这样 finalize() 可能永远都不会执行,所以指望它做收尾工作是靠不住的)
  • 执行 finalize 时,未捕获的异常会被忽略,并且 Finalizer 机制也会终止。未捕获的异常会使其它对象处于损坏状态,从而造成不确定行为。
  • 对象再生问题:finalize 方法中,可将待回收的对象赋值给 GC Roots 可达的对象引用,从而达到对象再生的目的
  • finalize 方法最多由 GC 执行一次(用户可以手动调用对象的 finalize 方法,但并不影响 GC 对 finalize 的行为)

引用一句话来总结 “finalizers are inherently problematic and their use can lead to performance issues, deadlocks, hangs, and other problematic behavior” 以及”the timing of finalization is unpredictable with no guarantee that a finalizer will be called.”,即它会导致性能问题,死锁,挂起等问题,而且不能保证它一定会被调用,它的调用时机是不可预测的。例如如果利用finalize关闭打开的文件时,因为系统的文件描述符是有限的,如果不能及时的关闭文件,会导致无法打开更多的文件。

flowchart

finalize 大致流程:当对象变成(GC Roots)不可达时,GC 会判断对象是否重写了 finalize() 方法,如果没有覆盖,则直接回收。否则,如果该对象没有执行 finalize 方法,则将其放入 F-Queue 队列,由一低优先级的线程执行该队列中对象的 finalize 方法。执行完成后会再次判断该对象是否可到达,若不可到达则回收,否则,对象“复活”。
finalize 具体流程:对象可有2种状态,涉及到两类状态空间:

  • 终结状态空间:unfinalized、finalizable、finalized
    • unfinalized:新建对象会先进入此状态,GC 并未准备执行 finalize 方法,因为对象是可到达的
    • finalizable:表示该对象已经不可到达了, GC 可对该对象执行 finalize 方法。GC 通过一线程来遍历 F-Queue 执行 finalize 方法
    • finalized:GC 已执行过 finalize 方法
  • 可达状态空间:reachable、finalizer-reachable、unreachable
    • reachable:表示 GC Roots 引用可达
    • finalizer-reachable(f-reachable):GC Roots 不可达,但通过某个 finalizable 对象可达
    • unreachable:对象不可通过上面两种途径可达

  1. A:创建一个对象,它会处于 reachable 和 unfinalized 状态
  2. 随着程序运行,一些引用关系会消失,导致状态变迁,从 reachable 状态变到 f-reachable(B、C、D) 或 unreachable(E、F)
  3. 若 JVM 检测到处于 unfinalized 状态的对象变成 f-reachable 或 unreachable,则会将其标记为 finalizable 状态(G、H)。即对象不是 GC Roots 可达的话,就将其标记为 finalizable。若对象处于[unreachable, finalized]状态,则同时将其标记为f-reachable(H)
  4. 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N)
  5. 处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable。这也是图中只有八个状态点的原因
  6. 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
  7. 若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)

sample

看下对象复活的例子:

public class FinalizeActive{

    public static FinalizeActive obj;

    @Override
    protected void finalize() throws Throwable {
        System.out.println("执行finalize");
        obj = this;
    }

    public static void main(String[] args) {

        obj = new FinalizeActive();

        //obj=null
        obj = null;
        System.gc();
        System.out.println("After GC");
        if(obj!=null)
            System.out.println("对象复活");

        obj = null;
        System.out.println("After GC");
        if(obj==null)
            System.out.println("对象死亡");

    }
}

输出:
执行 Finalize
After GC
对象复活
After GC
对象死亡

Solution

Java 9开始逐步使用 Cleaner 来替换掉 finalize。它的实现利用了幻想引用(PhantomReference),这时一种常见的所谓 post_mortem 清理机制。

Cleaner

简而言之,Cleaner 为对象提供了更简单的注册和注销的清理方法。它并不是用来完全去掉 finalization 或 sun.misc.Cleaner。当 GC 使用现有的四种引用机制发现对象不可到达时就会进行清理。它内部会维护一个线程来调用不可达的对象的清理方法。所注册的 clean 方法在不再需要的时候会被清理调,而且会立马执行清理工作。

Cleaner vs Finalizer

  • Cleaner 要优于 Finalizer 因为 Java 类的创建者可以控制它的 cleaner 机制的线程。但 cleaner 机制可以在后台执行,在 GC 的控制下运行,不能保证及时清理
  • Cleaner机制在出现异常的时候不会像 Finalizer 那样忽略并终止,因为使用Cleaner机制的类库可以控制其线程
  • Cleaner 和 Finalizer 清理类的所有实例都一样慢,它俩都会造成严重的性能损失。如果仅将它们用作安全网( safety net),则cleaner机制要快得多

Finalizer和Cleaner机制有什么好处

虽然上面把它们说的一无是处,但他们又2个合法用途:

  • 安全网:防止资源的拥有着忽略了它的 close 方法。虽然不能保证他们能迅速运行(或者根本就不会运行),但 better than never。在编写安全网之前,请仔细考虑敷出的代价值不值得。像 Java 的一些类库,如 FileInputStream、FileOutputStream、ThreadPoolExecutor和java.sql.Connection,都有作为安全网的Finalizer机制。
  • 合理使用Cleaner机制的方法与本地对等类(native peers)有关。本地对等类是一个由普通对象委托的本地(非Java)对象,假设性能是可以接受的,并且本地对等类没有关键的资源,那么Finalizer和Cleaner机制可能是这项任务的合适的工具。但如果性能是不可接受的,或者本地对等类持有必须迅速回收的资源,那么类应该有一个close方法,正如前面所述。

如何为对象封装需要结束的资源(如文件或线程),而不是为该类编写Finalizer和Cleaner机制

实现 AutoCloseable 方法,并要求客户在在不再需要时调用每个实例close方法,通常使用try-with-resources确保终止,即使面对有异常抛出情况(条目 9)。一个值得一提的细节是实例必须跟踪是否已经关闭:close方法必须记录在对象里不再有效的属性。

Sample

// An autocloseable class using a cleaner as a safety net
public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // Resource that requires cleaning. Must not refer to Room!
    private static class State implements Runnable {
        int numJunkPiles; 

        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }

        // Invoked by close method or cleaner
        @Override
        public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }

    // The state of this room, shared with our cleanable
    private final State state;

    // Our cleanable. Cleans the room when it’s eligible for gc
    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles) {
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override
    public void close() {
        cleanable.clean();
    }
}

run 方法在2种情况下触发:

  1. 调用 close 方法,它内部会调用 cleanable 的 clean 方法来触发。这是通常做法。
  2. 为避免没有手动调用 close 方法,那么 Cleaner 机制将(希望)调用 run 方法。它仅仅是作为一个安全网。

注意:State 必须为静态内部类,否则会阻止 Room 成为垃圾收集的资格。同样使用 lambda 也是不明智的,因为它很容易获得对宿主类对象的引用。

那么在调用的时候就可以将 Room 的实例放在 try-with-source 中,这样就永远不需要用安全网自动清理了:

public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("Goodbye");
        }
    }
}

它先会打印 “Goodbye”,然后会打印“Cleaning room”。

总结

除了作为一个安全网或者终止非关键的本地资源,不要使用Cleaner机制,或者是在Java 9发布之前的finalizers机制。即使是这样,也要当心不确定性和性能影响。

参考文献