1. 程式人生 > >Effective Java 第三版——8. 避免使用Finalizer和Cleaner機制

Effective Java 第三版——8. 避免使用Finalizer和Cleaner機制

roo 方式 主類 垃圾收集 exit 進行 alt 自動清理 很大的

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨著Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這裏第一時間翻譯成中文版。供大家學習分享之用。

技術分享圖片

8. 避免使用Finalizer和Cleaner機制

Finalizer機制是不可預知的,往往是危險的,而且通常是不必要的。 它們的使用會導致不穩定的行為,糟糕的性能和移植性問題。 Finalizer機制有一些特殊的用途,我們稍後會在這個條目中介紹,但是通常應該避免它們。 從Java 9開始,Finalizer機制已被棄用,但仍被Java類庫所使用。 Java 9中 Cleaner機制代替了Finalizer機制。 Cleaner機制不如Finalizer機制那樣危險,但仍然是不可預測,運行緩慢並且通常是不必要的。

提醒C++程序員不要把Java中的Finalizer或Cleaner機制當成的C ++析構函數的等價物。 在C++中,析構函數是回收對象相關資源的正常方式,是與構造方法相對應的。 在Java中,當一個對象變得不可達時,垃圾收集器回收與對象相關聯的存儲空間,不需要開發人員做額外的工作。 C ++析構函數也被用來回收其他非內存資源。 在Java中,try-with-resources或try-finally塊用於此目的(條目 9)。

Finalizer和Cleaner機制的一個缺點是不能保證他們能夠及時執行[JLS,12.6]。 在一個對象變得無法訪問時,到Finalizer和Cleaner機制開始運行時,這期間的時間是任意長的。 這意味著你永遠不應該Finalizer和Cleaner機制做任何時間敏感(time-critical)的事情。 例如,依賴於Finalizer和Cleaner機制來關閉文件是嚴重的錯誤,因為打開的文件描述符是有限的資源。 如果由於系統遲遲沒有運行Finalizer和Cleaner機制而導致許多文件被打開,程序可能會失敗,因為它不能再打開文件了。

及時執行Finalizer和 Cleaner機制是垃圾收集算法的一個功能,這種算法在不同的實現中有很大的不同。程序的行為依賴於Finalizer和 Cleaner機制的及時執行,其行為也可能大不不同。 這樣的程序完全可以在你測試的JVM上完美運行,然而在你最重要的客戶的機器上可能運行就會失敗。

延遲終結(finalization)不只是一個理論問題。為一個類提供一個Finalizer機制可以任意拖延它的實例的回收。一位同事調試了一個長時間運行的GUI應用程序,這個應用程序正在被一個OutOfMemoryError錯誤神秘地死掉。分析顯示,在它死亡的時候,應用程序的Finalizer機制隊列上有成千上萬的圖形對象正在等待被終結和回收。不幸的是,Finalizer機制線程的運行優先級低於其他應用程序線程,所以對象被回收的速度低於進入隊列的速度。語言規範並不保證哪個線程執行Finalizer機制,因此除了避免使用Finalizer機制之外,沒有輕便的方法來防止這類問題。在這方面, Cleaner機制比Finalizer機制要好一些,因為Java類的創建者可以控制自己cleaner機制的線程,但cleaner機制仍然在後臺運行,在垃圾回收器的控制下運行,但不能保證及時清理。

Java規範不能保證Finalizer和Cleaner機制能及時運行;它甚至不能能保證它們是否會運行。當一個程序結束後,一些不可達對象上的Finalizer和Cleaner機制仍然沒有運行。因此,不應該依賴於Finalizer和Cleaner機制來更新持久化狀態。例如,依賴於Finalizer和Cleaner機制來釋放對共享資源(如數據庫)的持久鎖,這是一個使整個分布式系統陷入停滯的好方法。

不要相信System.gcSystem.runFinalization方法。 他們可能會增加Finalizer和Cleaner機制被執行的幾率,但不能保證一定會執行。 曾經聲稱做出這種保證的兩個方法:System.runFinalizersOnExit和它的孿生兄弟Runtime.runFinalizersOnExit,包含致命的缺陷,並已被棄用了幾十年[ThreadStop]。

Finalizer機制的另一個問題是在執行Finalizer機制過程中,未捕獲的異常會被忽略,並且該對象的Finalizer機制也會終止 [JLS, 12.6]。未捕獲的異常會使其他對象陷入一種損壞的狀態(corrupt state)。如果另一個線程試圖使用這樣一個損壞的對象,可能會導致任意不確定的行為。通常情況下,未捕獲的異常將終止線程並打印堆棧跟蹤( stacktrace),但如果發生在Finalizer機制中,則不會發出警告。Cleaner機制沒有這個問題,因為使用Cleaner機制的類庫可以控制其線程。

使用finalizer和cleaner機制會導致嚴重的性能損失。 在我的機器上,創建一個簡單的AutoCloseable對象,使用try-with-resources關閉它,並讓垃圾回收器回收它的時間大約是12納秒。 使用finalizer機制,而時間增加到550納秒。 換句話說,使用finalizer機制創建和銷毀對象的速度要慢50倍。 這主要是因為finalizer機制會阻礙有效的垃圾收集。 如果使用它們來清理類的所有實例(在我的機器上的每個實例大約是500納秒),那麽cleaner機制的速度與finalizer機制的速度相當,但是如果僅將它們用作安全網( safety net),則cleaner機制要快得多,如下所述。 在這種環境下,創建,清理和銷毀一個對象在我的機器上需要大約66納秒,這意味著如果你不使用安全網的話,需要支付5倍(而不是50倍)的保險。

finalizer機制有一個嚴重的安全問題:它們會打開你的類來進行finalizer機制攻擊。finalizer機制攻擊的想法很簡單:如果一個異常是從構造方法或它的序列化中拋出的——readObject和readResolve方法(第12章)——惡意子類的finalizer機制可以運行在本應該“中途夭折(died on the vine)”的部分構造對象上。finalizer機制可以在靜態字屬性記錄對對象的引用,防止其被垃圾收集。一旦記錄了有缺陷的對象,就可以簡單地調用該對象上的任意方法,而這些方法本來就不應該允許存在。從構造方法中拋出異常應該足以防止對象出現;而在finalizer機制存在下,則不是。這樣的攻擊會帶來可怕的後果。Final類不受finalizer機制攻擊的影響,因為沒有人可以編寫一個final類的惡意子類。為了保護非final類不受finalizer機制攻擊,編寫一個final的finalize方法,它什麽都不做。

那麽,你應該怎樣做呢?為對象封裝需要結束的資源(如文件或線程),而不是為該類編寫Finalizer和Cleaner機制?讓你的類實現AutoCloseable接口即可,並要求客戶在在不再需要時調用每個實例close方法,通常使用try-with-resources確保終止,即使面對有異常拋出情況(條目 9)。一個值得一提的細節是實例必須跟蹤是否已經關閉:close方法必須記錄在對象裏不再有效的屬性,其他方法必須檢查該屬性,如果在對象關閉後調用它們,則拋出IllegalStateException異常。

那麽,Finalizer和Cleaner機制有什麽好處呢?它們可能有兩個合法用途。一個是作為一個安全網(safety net),以防資源的擁有者忽略了它的close方法。雖然不能保證Finalizer和Cleaner機制會迅速運行(或者根本就沒有運行),最好是把資源釋放晚點出來,也要好過客戶端沒有這樣做。如果你正在考慮編寫這樣的安全網Finalizer機制,請仔細考慮一下這樣保護是否值得付出對應的代價。一些Java庫類,如FileInputStreamFileOutputStreamThreadPoolExecutorjava.sql.Connection,都有作為安全網的Finalizer機制。

第二種合理使用Cleaner機制的方法與本地對等類(native peers)有關。本地對等類是一個由普通對象委托的本地(非Java)對象。由於本地對等類不是普通的 Java對象,所以垃圾收集器並不知道它,當它的Java對等對象被回收時,本地對等類也不會回收。假設性能是可以接受的,並且本地對等類沒有關鍵的資源,那麽Finalizer和Cleaner機制可能是這項任務的合適的工具。但如果性能是不可接受的,或者本地對等類持有必須迅速回收的資源,那麽類應該有一個close方法,正如前面所述。

Cleaner機制使用起來有點棘手。下面是演示該功能的一個簡單的Room類。假設Room對象必須在被回收前清理幹凈。Room類實現AutoCloseable接口;它的自動清理安全網使用的是一個Cleaner機制,這僅僅是一個實現細節。與Finalizer機制不同,Cleaner機制不汙染一個類的公共API:

// 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; // Number of junk piles in this room

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

靜態內部State類擁有Cleaner機制清理房間所需的資源。 在這裏,它僅僅包含numJunkPiles屬性,它代表混亂房間的數量。 更實際地說,它可能是一個final修飾的long類型的指向本地對等類的指針。 State類實現了Runnable接口,其run方法最多只能調用一次,只能被我們在Room構造方法中用Cleaner機制註冊State實例時得到的Cleanable調用。 對run方法的調用通過以下兩種方法觸發:通常,通過調用Roomclose方法內調用Cleanableclean方法來觸發。 如果在Room實例有資格進行垃圾回收的時候客戶端沒有調用close方法,那麽Cleaner機制將(希望)調用Staterun方法。

一個State實例不引用它的Room實例是非常重要的。如果它引用了,則創建了一個循環,阻止了Room實例成為垃圾收集的資格(以及自動清除)。因此,State必須是靜態的嵌內部類,因為非靜態內部類包含對其宿主類的實例的引用(條目 24)。同樣,使用lambda表達式也是不明智的,因為它們很容易獲取對宿主類對象的引用。

就像我們之前說的,Room的Cleaner機制僅僅被用作一個安全網。如果客戶將所有Room的實例放在try-with-resource塊中,則永遠不需要自動清理。行為良好的客戶端如下所示:

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

正如你所預料的,運行Adult程序會打印Goodbye字符串,隨後打印Cleaning room字符串。但是如果時不合規矩的程序,它從來不清理它的房間會是什麽樣的?

public class Teenager {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("Peace out");
    }
}

你可能期望它打印出Peace out,然後打印Cleaning room字符串,但在我的機器上,它從不打印Cleaning room字符串;僅僅是程序退出了。 這是我們之前談到的不可預見性。 Cleaner機制的規範說:“System.exit方法期間的清理行為是特定於實現的。 不保證清理行為是否被調用。”雖然規範沒有說明,但對於正常的程序退出也是如此。 在我的機器上,將System.gc()方法添加到Teenager類的main方法足以讓程序退出之前打印Cleaning room,但不能保證在你的機器上會看到相同的行為。

總之,除了作為一個安全網或者終止非關鍵的本地資源,不要使用Cleaner機制,或者是在Java 9發布之前的finalizers機制。即使是這樣,也要當心不確定性和性能影響。

Effective Java 第三版——8. 避免使用Finalizer和Cleaner機制