天天看點

【第8條】避免使用終結方法和清除方法避免使用終結方法和清除方法

避免使用終結方法和清除方法

最終方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。 它們的使用會導緻不穩定的行為,糟糕的性能和移植性問題。 Finalizer 機制有一些特殊的用途,我們稍後會在這個條目中介紹,但是通常應該避免它們。從 Java 9 開始,Finalizer 機制已被棄用,但仍被 Java 類庫所使用。 Java 9 中Cleaner 機制代替了 Finalizer 機制。清除方法沒有終結方法那麼危險,但仍然是不可預測、運作緩慢,一般情況下也是不必要的

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

Finalizer 和 Cleaner 機制的一個缺點是不能保證他們能夠及時執行[JLS,12.6]。在一個對象變得無法通路時,到 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語言規範不僅不保證終結方法或者清除方法會被及時地執行,而且根本就不保證它們會被執行。當一個程式終止的時候,某些已經無法通路的對象上的終結方法卻根本沒有被執行,這是完全有可能的。結論是:永遠不應該依賴終結方法或者清除方法來更新重要的持久狀态。例如,依賴終結方法或者清除方法來釋放共享資源(比如資料庫)上的永久這很容易讓整個分布式系統垮掉。

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

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 attack)打開了類的大門。終結方法攻擊背後的思想很簡單:如果從構造器或者它的序列化對等體( readObject 和 readResolve方法,詳見第12章)抛出異常,惡意子類的終結方法就可以在構造了部分的應該已經半途天折的對象上運作。這個終結方法會将對該對象的引用記錄在一個靜态域中,阻止它被垃圾回收。一旦記錄到異常的對象,就可以輕松地在這個對象上調用任何原本永遠不允許在這裡出現的方法。從構造器抛出的異常,應該足以防止對象繼續存在;有了終結方法的存在,這一點就做不到了。這種攻擊可能造成緻命的後果。 final類不會受到終結方法攻擊,因為沒有人能夠編寫出 final類的惡意子類。為了防止非fnal類受到終結方法攻擊,要編寫一個空的final的finalize方法。

那麼,你應該怎樣做呢?為對象封裝需要結束的資源(如檔案或線程),而不是為該類編寫Finalizer 和 Cleaner 機制?讓你的類實作 AutoCloseable 接口即可,并要求客戶在在不再需要時調用每個執行個體 close 方法,通常使用 try-with-resources 確定終止,即使面對有異常抛出情況(詳見第 9條)。一個值得一提的細節是,該執行個體必須記錄下自己是否已經被關閉了:close 方法必須記錄在對象裡不再有效的屬性,其他方法必須檢查該屬性,如果在對象關閉後調用它們,則抛出 IllegalStateException 異常。

那麼,Finalizer 和 Cleaner 機制有什麼好處呢?它們可能有兩個合法用途。一個是作為一個安全網(safety net),以防資源的擁有者忽略了它的 close 方法。雖然不能保證 Finalizer 和 Cleaner 機制會迅速運作 (或者根本就沒有運作),最好是把資源釋放晚點出來,也要好過用戶端沒有這樣做。如果你正在考慮編寫這樣的安全網 Finalizer 機制,請仔細考慮一下這樣保護是否值得付出對應的代價。一些 Java庫類,如 FileInputStream 、 FileOutputStream 、 ThreadPoolExecutor 和 java.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 方法的調用通過以下兩種方法觸發:通常,通過調用 Room 的 close 方法内調用 Cleanable 的 clean 方法來觸發。如果在 Room 執行個體有資格進行垃圾回收的時候用戶端沒有調用 close 方法,那麼 Cleaner 機制将(希望)調用 State 的 run 方法。

一個 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 ,但不能保證在你的機器上會看到相同的行為。

總而言之,處分是作為安全網,或者是為了終止非關鍵的本地資源,否則請不要使用清除方法,對于在Java9之前的發行版本,則盡量不要使用終結方法。若使用了終結方法或者清除方法,則要注意它的不确定性和性能後果

                                                                                             關注公衆号

                                                                                            每天幹貨分享

【第8條】避免使用終結方法和清除方法避免使用終結方法和清除方法