1. 程式人生 > >Java基礎四:避免使用finalize()方法

Java基礎四:避免使用finalize()方法

finalize()是在java.lang.Object裡定義的,也就是說每一個物件都有這麼個方法。這個方法在gc啟動,該物件被回收的時候被呼叫。其實gc可以回收大部分的物件(凡是new出來的物件,gc都能搞定,一般情況下我們不會用new以外的方式去建立物件),所以一般是不需要程式設計師去實現finalize的。

1、概述

1、終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。是使用終結方法會導致行為不穩定、降低效能,以及可移植性問題。所以,我們應該避免使用終結方法。
2、使用終結方法有一個非常嚴重的效能損失。在我的機器上,建立和銷燬一個簡單物件的時間大約為5.6ns、增加一個終結方法使時間增加到了2400ns。換句話說,用終結方法建立和銷燬物件慢了大約430倍。
3、如果實在要實現終結方法,要記得呼叫super.finalize()

上面的3點是出自Effective Java第二版第七條中的部分內容,可能剛開始我們看的時候一臉懵逼。有的人甚至都沒聽過finalize方法,更不知道用了它會出現什麼問題了。下面我們來說說finalize方法。

2、Object中的finalize方法

protected void finalize() throws Throwable { }

我們可以看到Object中的finalize方法什麼都沒有實現,而且修飾符是protected,明顯可以看出來是由子類去實現它的。這個方法的原意是在GC發生時銷燬一些資源使用的,那麼什麼時候會呼叫這個方法呢?

原來在類載入的時候,會去檢查一個類是否含有一個引數為空,返回值為void的finalize方法,還要求finalize方法必須非空。這個類我們暫時稱為finalizer類(簡稱f類)。

3、註冊finalizer類

比如我們有一個類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;
    }
}

4、呼叫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();
}

5、finalize方法導致記憶體溢位

網上很多文章講的很明白了:
Java的Finalizer引發的記憶體溢位
過載Finalize引發的記憶體洩露
主要原因是:Finalizer執行緒會和我們的主執行緒進行競爭,不過由於它的優先順序較低,獲取到的CPU時間較少,因此它永遠也趕不上主執行緒的步伐。所以最後會發生OutOfMemoryError異常。

6、結論

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中使用的就是這個。