Effective Java 3rd 條目8 避免使用finalizer和cleaner

Finalizer是不可預(yù)測的,常常是危險的,而且通常來說是不必要的。它的使用會引起不穩(wěn)定的行為、糟糕的性能和可移植性問題。Finalizer有一些正當(dāng)?shù)挠梅ǎ覀儗⒃谶@個條目后面講述,但是一般來說,你應(yīng)該避免使用它。在Java 9中,finalizer被棄用了,但是它仍然在Java庫里面使用著。Java 9中finalizer的替代是cleaner。Cleaner危險性比finalizer小,但仍然是不可預(yù)測的、慢的和通常來說不必要的

C++程序員被警告,不要把Java中的finalizer或者cleaner看成C++析構(gòu)子(destructor)的類比。C++中,析構(gòu)子是標(biāo)準(zhǔn)的方式回收和對象相關(guān)的資源,是一個和構(gòu)造子必要的對應(yīng)物。Java中,當(dāng)對象不可達(dá)時,程序員方面不需要做特別的工作,垃圾回收器會回收和對象相關(guān)的內(nèi)存。C++析構(gòu)子也用來回收其他的非內(nèi)存資源。在Java中,try-with-resources或者try-finally代碼塊也是用在這個目的(條目 9)。

finalizer和cleaner的缺點是,不能保證它們被立即執(zhí)行[JLS, 12.6]。在一個對象成為不可到達(dá)的時間,和它的finalizer或者cleaner執(zhí)行的時間之間,可能存在任意長的時間。這意味著,你永遠(yuǎn)不要在finalizer或者cleaner里面做時間緊要的任何事情。比如,依賴一個finalizer或者cleaner關(guān)閉文件,這是一個嚴(yán)重的錯誤,因為打開文件描述符是一個有限的資源。如果因為系統(tǒng)延遲運(yùn)行finalizer和cleaner,而很多文件被打開著,那么一個程序可能失敗因為它不再能打開文件。

finalizer和cleaner被執(zhí)行的及時性主要是一個垃圾回收算法的功能,這對于不同的實現(xiàn)有千差萬別。一個代碼的行為,依賴于finalizer或者cleaner執(zhí)行的及時性,同樣可能不盡相同。一個程序完美運(yùn)行在JVM上,而且你測試了它,然后在一個你最重要客戶喜歡的JVM上糟糕地失敗了,這是完全有可能的。

延遲終止化不只是一個理論上的問題。為一個類提供finalizer可以任意延遲對它的對象的回收。一個同事調(diào)試了一個長期運(yùn)行的GUI應(yīng)用,以難以理解的OutOfMemoryError方式死亡。分析指出,在它死亡的時候,這個應(yīng)用在finalizer隊列上有成千個圖形對象,剛好等著被終止和回收,不幸的是,finalizer線程比另外的應(yīng)用線程運(yùn)行在更低的優(yōu)先級,所以對象沒有以它們成為符合終止化條件的速度被終止。語言規(guī)范沒有保證哪個線程執(zhí)行finalizer,所以沒有跨平臺的方式來阻止這樣的問題,只有避免使用finalizer。cleaner比finalizer在這一點上好些,因為類的作者有對他們自己cleaner線程的控制。但是cleaner仍然運(yùn)行在后臺,在垃圾回收器的控制之下,所以沒有保證立即清理。

不僅是規(guī)范沒有提供finalizer或者cleaner將立即運(yùn)行的保證,而且也沒有提供它們到底會不會運(yùn)行的保證。在某些不可到達(dá)的對象上,一個程序終止而沒有運(yùn)行它們,這是完全有可能的,甚至很有可能的。因此,你應(yīng)該從不依賴一個finalizer或者cleaner來更新持久化狀態(tài)。比如,依賴finalizer或者cleaner來釋放一個在共享資源(比如數(shù)據(jù)庫)上的持久化鎖,這是一個把你整個分布式系統(tǒng)戛然而止的非常好的方式。

不要被System.gc和System.runFinalization方法所引誘。它們可能會增加finalizer或者cleaner的執(zhí)行幾率,但是它們不能保證。這兩個方法是曾經(jīng)是聲稱這個保證:System.runFinalizersOnExit和它的邪惡孿生化身Runtime.runFinalizersOnExit。這些方法有致命的缺陷,已經(jīng)被廢棄數(shù)十年了 [ThreadStop]。

finalizer的另外一個問題是,在終止過程中一個未捕獲異常是被忽略的,這個對象的終止化也會停止[JLS, 12.6]。未捕獲異常使得其他對象處于破壞的狀態(tài)。如果另外一個線程嘗試使用這樣的破壞對象,會導(dǎo)致任意非確定性的行為。通常地,一個未捕獲異常將終止線程,然后打印堆疊信息,但是如果發(fā)生在finalizer中則不會,它甚至不會打印一個警告。cleaner沒有這樣的問題,因為使用cleaner的庫有對它線程的控制。

使用finalizer和cleaner有嚴(yán)重的性能懲罰。在我的機(jī)器上,創(chuàng)建簡單的AutoCloseable對象,用try-with-resources關(guān)閉它,和垃圾回收器回收它,這段時間是大約2 ns。相反,使用finalizer的時間增加到550 ns。換句話說,創(chuàng)建和用finalizer銷毀對象,大約慢50倍。這種要是因為finalizer抑制了有效率的垃圾回收。如果你使用它們來清理類的所有實例(在我的機(jī)器上每個實例大約500 ns),cleaner在速度上和finalizer是可比的,但是就像下面討論的,如果僅僅使用它們作為安全網(wǎng)絡(luò),cleaner會更快。這這些情況下,創(chuàng)建,清理和銷毀一個對象在我的機(jī)器上花了66 ns,這意味著,如果你不使用它,你將為安全網(wǎng)絡(luò)保障花費(fèi)一個因素5(而不是50)。

finalizer有嚴(yán)重的問題:它們使得你的類受finalizer攻擊。finalizer攻擊背后的思想很簡單:如果一個異常從構(gòu)造子或者它的序列化等同物(即readObject和readResolve方法(12章))拋出,惡意的子類的finalizer可以運(yùn)行在部分構(gòu)造的對象上,這個對象本應(yīng)該胎死腹中。finalizer可以在靜態(tài)域中記錄對對象的引用,防止它被垃圾回收。一旦這個惡意的對象被記錄,調(diào)用這個對象的任意方法是易如反掌的事情,這個對象起初從來不允許存在。從構(gòu)造子拋出一個異常應(yīng)該足夠防止一個對象存在。在finalizer面前,則不是。這樣的攻擊可能有可怕的后果。final類是免于finalizer攻擊的,因為沒有人可以編寫一個final類的惡意子類。為了保護(hù)非final類免于finalizer攻擊,編寫一個final的不做任何事情的finalizer方法

那么,對于一個類,它的對象封裝了需要終止的資源,比如文件或者線程,不要編寫finalizer或者cleaner,你應(yīng)該怎么做?僅僅只要讓你的類實現(xiàn)AutoCloseable,當(dāng)每個實例不再需要的時候,要求它的客戶端調(diào)用它的close方法,甚至在面對異常時,通常用try-with-resources來保證終止(條目11)。一個值得提起的細(xì)節(jié)是,實例必須跟蹤它是否被關(guān)閉:close方法必須在一個域中記錄這個對象已經(jīng)不再有效,而且其他的方法必須檢測這個域,如果它們在對象關(guān)閉之后被調(diào)用,拋出一個IllegalStateException。

那么,如果有的話,cleaner和finalizer對什么有用呢?或許它們有兩個正當(dāng)?shù)挠猛尽R粋€是作為安全保障,以防資源的擁有者忽略了調(diào)用它的close方法。雖然沒有保證cleaner或者finalizer立即運(yùn)行(或者究竟會不會),如果客戶端沒有怎么做,晚清資源空比沒有清空要好。如果你考慮編寫這樣一個安全保障的finalizer,需要深思熟慮這個防護(hù)是否值得這個代價。一些Java庫類,比如FileInputStream、FileOutputStream、ThreadPoolExecutor和java.sql.Connection,有作為安全保障的finalizer。

第二種適合的用途與對象的本地對等體(native peer)有關(guān)。本地對等體是一個本地對象,普通對象通過本地方法(native method)委托給一個本地對象。

cleaner的第二個適合的用途與對象的本地對等體(native peer)有關(guān)。本地對等體是一個本地(非Java)對象,一個普通對象通過本地方法(native method)委托給一個這個本地對象。因為本地對等體不是一個普通的對象,垃圾回收器不知道它,所以當(dāng)它的Java對等體被回收時不能夠回收它。cleaner或者finalizer可能是這個任務(wù)的一個合適方案,假如性能是可以接受的,而且本地對等體沒有保留關(guān)鍵資源。如果性能是不可接受的,或者本地對等體保留了必須立即回收的資源,這個類應(yīng)該有個close方法,就像前面描述的一樣。

cleaner有點難于使用。下面是一個簡單的展現(xiàn)這種技巧的Room類。讓我們假設(shè)房間在它們被回收之前必須被清理。Room類實現(xiàn)了AutoCloseable,它的自動清理安全保障使用了一個cleaner,這個事實僅僅是一個實現(xiàn)細(xì)節(jié)。不像finalizer,cleaner不會污染公開的API:

// 一個使用cleaner作為安全保障的autocloseable類 
public class Room implements AutoCloseable { 
    private static final Cleaner cleaner = Cleaner.create();

    // 需要清理的資源。必須不引用Room!
    private static class State implements Runnable { 
        int numJunkPiles; // 這個房間里面的垃圾堆數(shù)量

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

        // 被close方法或者cleaner調(diào)用 
        @Override public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0; 
        }
    }

    // 這個房間的狀態(tài),與我們的cleanable共享
    private final State state;

    // 我們的Cleanable. 當(dāng)它符合垃圾回收條件時,清理房間
    private final Cleaner.Cleanable cleanable;

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

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

靜態(tài)內(nèi)嵌State類持有資源,cleaner需要這個資源清理清理房間。在這個例子中,它僅僅是numJunkPiles域,表示這個房間的混亂程度。更逼真地,它可是是一個final長整形,包含了一個對本地對等體的指針。State實現(xiàn)了Runnable,而且它的run方法最多只被調(diào)用一次,由Cleanable調(diào)用,當(dāng)在Room構(gòu)造子中利用我們的cleaner,注冊我們的State實例時,我們獲得這個調(diào)用。run方法的調(diào)用y由這兩件事情之一觸發(fā):通常,它由一個調(diào)用Room的close方法觸發(fā),這個close方法調(diào)用Cleanable的clean方法。如果客戶端到Room對象適合垃圾回收的時候不能夠調(diào)用close方法,cleaner將調(diào)用(希望如此)State的run方法。

State實例沒有引用它的Room實例,這是很重要的。如果是這樣的話,它會創(chuàng)建一個循環(huán),將阻止Room對象適合垃圾回收(和自動被清理)。所以,State必須是一個靜態(tài)嵌套類,因為非靜態(tài)嵌套類包含對它們的宿主類實例(enclosing instance)的引用(條目24)。相似地,使用一個lambda表達(dá)式是不明智的,因為它們可能很容易地獲取宿主類對象(enclosing object)的引用。

就像我們前面說過的,Room的cleaner僅僅作為一個安全保障來使用的。如果在 try-with-resource代碼塊中,客戶端包圍所有Room實例化,自動清理沒有必要。這個行為良好的客戶端顯示了這個行為:

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

就像你可以預(yù)料的,運(yùn)行Adult程序打印了Goodbye,接著“Cleaning room”。但是這個行為有誤的代碼怎么樣呢,代碼永遠(yuǎn)不會清理它的房間?

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

你可能預(yù)想它會打印出“Peace out”,接著“Cleaning room”,但是在我的機(jī)器上,它不會打印“Cleaning room”,程序只是退出了。這是我們前面說過的不可預(yù)測性。Cleaner文檔說,“在System.exit的期間,cleaner的行為是依賴特定實現(xiàn)的,關(guān)于清理是否調(diào)用,沒有保證”。雖然文檔沒有說明,對于正常的程序退出,同樣是適用的。在我的機(jī)器中,添加System.gc()這行到Teenager的main方法,足夠使得它在退出前打印“Cleaning room”,但是這沒有保證在你的機(jī)器上可以看見同樣的行為。

總之,不要用cleaner,或者在早于Java 9的發(fā)布中的finalizer,除了作為安全保障或者終止非關(guān)鍵的本地資源之外。即使那樣,當(dāng)心不確定性和性能后果。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容