避免使用Finalizer和Cleaner機制
finalize機制自己就是存在問題的。
finalize機制可能會致使性能問題,死鎖和線程掛起。
finalize中的錯誤可能致使內存泄漏;若是不在須要時,也沒有辦法取消垃圾回收;而且沒有指定不一樣執行finalize對象的執行順序。此外,沒有辦法保證finlize的執行時間。
遇到這些狀況,對象調用finalize方法只有被無限期延後。java
Java9中finalize方法已經被廢棄。
---------------------
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機制而致使許多文件被打開,程序可能會失敗,由於它不能再打開文件了。sql
及時執行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.gc和System.runFinalization方法。 他們可能會增長Finalizer和Cleaner機制被執行的概率,但不能保證必定會執行。 曾經聲稱作出這種保證的兩個方法:System.runFinalizersOnExit和它的孿生兄弟Runtime.runFinalizersOnExit,包含致命的缺陷,並已被棄用了幾十年[ThreadStop]。ide
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庫類,如FileInputStream、FileOutputStream、ThreadPoolExecutor和java.sql.Connection,都有做爲安全網的Finalizer機制。例如FileOutputStream
第二種合理使用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,但不能保證在你的機器上會看到相同的行爲。
總之,除了做爲一個安全網或者終止非關鍵的本地資源,不要使用Cleaner機制,或者是在Java 9發佈以前的finalizers機制。即便是這樣,也要小心不肯定性和性能影響。