Java 避免使用Finalizer和Cleaner機制

避免使用Finalizer和Cleaner機制
finalize機制本身就是存在問題的。
finalize機制可能會導致性能問題,死鎖和線程掛起。
finalize中的錯誤可能導致內存泄漏;如果不在需要時,也沒有辦法取消垃圾回收;並且沒有指定不同執行finalize對象的執行順序。此外,沒有辦法保證finlize的執行時間。
遇到這些情況,對象調用finalize方法只有被無限期延後。

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機制而導致許多文件被打開,程序可能會失敗,因爲它不能再打開文件了。

及時執行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]。

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機制。即使是這樣,也要當心不確定性和性能影響。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章