天天看點

《Effective Java》讀書筆記06--避免使用終結方法

終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。

一、終結方法VS析構器

熟悉C++的都知道,析構器是用來回收一個對象所占用資源的正常方法,是構造器所必需的對應物。在JAVA中,當一個對象變得不可達時,垃圾回收器會回收與該對象關聯的存儲空間,這不需要我們操心。對于非記憶體資源的回收,C++析構器是可以管理的,而JAVA的垃圾回收器是不會管理這些非記憶體資源的,我們通常使用try-finally塊來顯式管理這些資源,比如檔案操作,socket連接配接等。

二、終結方法的劣行

1、行為不穩定

終結方法不能保證被及時地執行,有時甚至根本不會被執行。從一個對象變成不可到達開始,到它的終結方法被執行,所花費的時間是任意長的。如果在終結方法中執行檔案關閉操作,很有可能造成程式因為不能打開檔案而出現運作錯誤,因為終結方法執行時間是任意的,而系統檔案打開數是一定的,當系統檔案打開數達到最大時,程式就會抛出Open too many files異常,這是系統級錯誤,是以不能依靠終結方法來處理這些有限資源的釋放。另外,也不能依賴終結方法來更新重要的持久狀态。例如,依賴終結方法解放共享資源(比如資料庫)上的永久鎖,很容易讓整個分布式系統垮掉。

2、可移植性問題

及時執行終結方法是垃圾回收算法的一個主要功能,而每個JVM實作中,垃圾回收算法是不一樣的,這就導緻終結方法在不同JVM實作下表現的差異,這種差異可能帶來災難性影響。

3、性能損失

使用終結方法銷毀對象需要依賴于JVM的垃圾回收算法排程,這樣必将造成銷毀對象耗時更多,降低性能。

三、終結方法的誘惑

對于初學者來說,特别是不了解JVM垃圾回收思想的同學,比如我,經常會使用System.gc()來處理一些邏輯,這不是最佳程式設計實踐,我們最好不要這樣做。System.gc()和System.runFinalization()這兩個方法确實能夠增加終結方法被執行的機會,但它們并不能保證終結方法一定會被執行。唯一聲稱保證終結方法被執行的方法是System.runFinalizersOnExit和Runtime.runFinalizerOnExit,但這兩個方法都有緻命的缺陷,已經被廢棄了,聽着很吓人哈。

四、終結方法的合理使用場景

1、當對象的所有者忘記調用顯示終止方法時,終結方法可以充當"安全網"。

顯示終止方法是提供一個顯式地終止方法,并要求該類的用戶端在每個執行個體不在有用的時候調用這個方法,比如InputStream、Outputstream和java.sql.Connection的close方法。java.util.Timer的cancel方法,它會執行必要的狀态改變,使得與Timer執行個體相關聯的線程溫和得終止自己。Image.flush會釋放所有與Image執行個體相關聯的資源,但該執行個體仍然處于可用的狀态,如果有必要的話,會重新配置設定資源。

顯式終止方法所在類的執行個體必須記錄自己是否已經被終止,這樣做主要是為了防止多次調用終止方法。當類執行個體被終止之後,再調用終止方法會抛出IllegalStateException異常(這個需要類實作者實作,隻需判斷類終止标志是否顯式該對象已經無效,無效則抛異常)。

使用終結方法充當"安全網"雖然不能保證終結方法會被及時調用,但在用戶端無法通過調用顯式的終止方法來正常結束操作的情況下,遲一點釋放關鍵資源總比永遠不釋放要好,此時最好将資源未釋放的情況記錄到日志中,以便修複bug。

2、通過本地方法與本地對象互動時

Java對象通過本地方法将某操作委托給一個本地對象時,如果本地對象不擁有關鍵資源,則可以在終結方法中執行這項任務。如果本地對象擁有必須被及時終止的資源,那麼該類就應該具有一個顯式的終止方法,在該方法中完成所有必要的操作以便釋放關鍵資源。終止方法可以是本地方法,也可以調用本地方法。

五、終結方法守衛者

終結方法守衛者是用來終結它的外圍執行個體。因為,“終結方法鍊”不會被自動執行。如果父類有終結方法,并且子類覆寫了終結方法,那麼子類的終結方法必須手工調用超類的終結方法。

protected void finalize() throws Throwable{
try{
 ....//Finalize subclass state
}finally{
super.finalize();
}

}
           

這樣可以保證: 即使子類的終結過程抛出異常,超類的終結方法也會得到執行。反之亦然。

如果子類實作者覆寫類超類的終結方法,但忘了手工調用終結方法(或者有意選擇不調用超類的終結方法),那麼超類的終結方法将永遠不會被調用到。終結方法守護者就是為了防止此類狀況的發生,做法是為每個将被終結的對象建立一個附加的對象,并把終結方法放到這個匿名的對象中,由于外圍執行個體會在它的私有執行個體域中儲存一個對其終結方法守衛者的唯一引用,是以終結方法守衛者與外圍執行個體可以同時啟動終結過程(垃圾回收器在回收外圍執行個體時會将它關聯的無用對象回收),而這個終結行為正是外圍對象所期望的。

//Finalizer Guardian idiom
public class Foo{
   private final Object finalizerGuardian = new Object(){
@Override protected void finalize() throws Throwable{
  ...//終結外圍執行個體
}
};
...//Remainder omitted
}
           

注意:公有類Foo并沒有終結方法(除了它從Object繼承過來的一個無關緊要的之外),是以子類的終結方法是否調用super.finalize并不重要。對于每一個帶有終結方法的非final公有類,都應該考慮使用終結方法守衛者模式。

六、最佳程式設計實踐

1、除非作為安全網,或為了終止非關鍵的本地資源,否則不要使用終結方法。

2、如果使用終結方法,就要記住調用super.finalize

3、如果需要把終結方法與公有的非final類關聯起來,需考慮使用終結方法守衛者,以確定即使子類的終結方法未能調用super.finalize,該終結方法也會被執行。