Effective Java 讀書筆記(7)避免finalizer

7.Avoid finalizers

大意為 避免 ”終結(jié)者“(finalizer)

Finalizers是不可預料的,經(jīng)常是危險的并且經(jīng)常是沒有必要的

對于Finalizers他們的使用可能會造成錯誤的產(chǎn)生,糟糕的性能以及移植性的問題,當然Finalizers有著一些有用的優(yōu)點,我們會在后續(xù)介紹這些,但是作為首要的規(guī)則,你應該避免finalizers

C++程序編寫者們被警告不要去如同C++析構(gòu)對于Java的模擬來考慮finalizers,在C++之中,析構(gòu)函數(shù)是經(jīng)常被用來作為回收對象間關(guān)聯(lián)資源的方法,作為構(gòu)造函數(shù)的反面,但是在Java之中,垃圾回收收集器會在一個對象變得不可達的時候回收它的相關(guān)資源,對于程序員這部分來說并不需要特別地去添加,C++的析構(gòu)也通常被用來回收其他的非內(nèi)存資源,Java中try-finally的塊就是經(jīng)常用作這個目的。

一個對于finalizers的缺點就是并沒有什么保證他們會及時被執(zhí)行,直到一個對象變得不可達,finalizer可以很久之后才執(zhí)行,這就意味著你應該對于時間嚴格的任務就不用考慮finalizer了,舉個例子,有一個嚴重的錯誤取決于一個關(guān)閉文件的finalizer,因為打開文件的描述是一個有限制的資源,如果太多文件需要打開由于JVM執(zhí)行finalizer比較遲緩,由于再也無法打開文件 一個程序可能就會掛掉了

哪一個finalizer會被執(zhí)行的及時性主要是是垃圾回收器算法的一個功能,這個算法在JVM的實現(xiàn)中變化很大,一個依賴于finalizer的及時性執(zhí)行的程序的表現(xiàn)可能同樣變化,很有可能的是這樣的一個程序在你測試的JVM上運行良好,而在你最重要的顧客喜歡的JVM上失敗了

延遲的finalization并不僅僅是理論上的一個問題,為一個類提供一個finalizer,在極少數(shù)情況下,對于該實例的任意的回收會延遲,一個部門在調(diào)試一個運行了很久的GUI應用時突然該應用奇怪地掛掉了并且產(chǎn)生了一個內(nèi)存溢出錯誤(OutOfMemoryError /OOM),通過對這個程序掛掉地時候的分析來看,這個應用有著成千上萬的圖像對象在它的finalizer隊列中,這些finalizer正在等待被finalized掉或者回收掉,不幸運的是,這個finalizer線程的優(yōu)先級相較于其他線程來說實在是比較低,故那些對象并沒有被finalize掉直到他們的finalize線程足夠優(yōu)先了

語言上的規(guī)范使得對于哪個線程來執(zhí)行finalizer沒有保證,故并沒有便捷的方法來避免這個問題,所以避免使用finalizer還是比較實用的

不僅僅語言上的規(guī)范對finalizer的及時執(zhí)行提供不了保證,它同時也無法保證這些finalizer會全部被執(zhí)行,一個程序不執(zhí)行finalizer在某些已經(jīng)不可達了的對象上而終止了的情況是絕對有可能存在的,故我們應該永不依賴于finalizer去更新一些嚴格持久的狀態(tài),舉個例子,利用finalizer來在分享了的資源上釋放一個持久的鎖,比如一個數(shù)據(jù)庫,這樣的確是個好方式萊斯利的整個分布式系統(tǒng)逐漸停止掉~

不要被System.gc和System.runFinalization所誘騙到,他們可能增加了fina這兩個方法lizer被執(zhí)行的概率,但是他們并不能保證執(zhí)行,唯一的能夠保證finalization的方法就是System.runFinalizersOnExit還有它的邪惡的雙胞胎 Runtime.runFinalizersOnExit,這兩個方法有著致命的缺陷[線程終止]并且已經(jīng)被棄用了

如果你還覺得避免finalizer這并不是特別令人信服的話,這里有其他的一些值得考慮的情況:

  • 如果一個沒有捕獲到的異常被拋出在finalization的時候,這個異常會被忽略,并且那個對象的finalization會中止
  • 沒有捕獲到的異常會使對象在一個污濁的狀態(tài)之中,如果其他線程其他使用這樣的一個污濁狀態(tài)的對象,可能會導致任意的非確定性后果

更為普通地來說,一個沒有捕獲的異常會終止線程并且把堆棧跟蹤打印出來,但是如果在finalizer里面的話,最慘的結(jié)果就是什么也沒有打印出來,甚至是一個警告

還有一件事,使用finalizer有一個十分嚴重的懲罰性的表現(xiàn),在我的機子上,創(chuàng)建一個簡單的對象需要花費5.6ns,如果加上finalizer就變成了2400ns,換句話說,這就是430倍的增長

那么,如果不使用finalizer我們該如何去處理一個含有封裝資源并且需要被終止的類呢,比如文件或者線程,解決的辦法就是提供一個顯式的終止方法,并且要求這個類的用戶對于每個這樣的已經(jīng)不需要了的實例去調(diào)用這個方法,值得提的一件事就是我們必需時刻知道它是否被終止了,這個顯式終止方法必需寫在一個private的域里 面,當這個對象不再有效,其他的方法必需檢查這個域并且當這個類被終止后又被調(diào)用了的時候拋出一個IllegalStateException的異常

典型的關(guān)于顯式的終止方法的例子就是InputStream,OutputStream和java.sql.Connection里面的close方法,其他的例子比如java.util.Timer里面的cancel方法,這個方法在對于使關(guān)聯(lián)Timer實例的線程自身輕柔地終止上表現(xiàn)出必要的狀態(tài)轉(zhuǎn)變

java.awt包括Graphics.dispose和Window.dispose的例子,這些方法經(jīng)常被忽略,從而表現(xiàn)出可怕的結(jié)果

一個相關(guān)的方法就是Image.flush,這個方法解除了所有關(guān)于Image實例相關(guān)的資源的分配,但是對于該實例保留了一個仍然可用的狀態(tài),如果需要的話會再一次重新分配資源

顯式終止方法在try-finally結(jié)構(gòu)的組合上被特別地使用來保證終止,在finally的塊中調(diào)用顯式的終止方法會使得它會被執(zhí)行即使當這個對象正在被使用的時候一個異常被拋出

// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
     foo.terminate(); // Explicit termination method
}

所以,到這了你還覺得finalizer好?這可能有兩種的使用,一種是在一個對象的擁有者忘記調(diào)用顯式終止方法的時候作為一個”安全網(wǎng)“,即使對于這個finalizer會不會及時地調(diào)用并沒有保證,但是有總好過沒有,在這種情況下finalizer如果發(fā)現(xiàn)資源還沒有被終止地話必需log一個警告出來,作為bug的提示,如果你考慮編寫這么一個安全網(wǎng)finalizer的話,對于一些額外的維護所造成的代價你也必須考慮一下

有這么四個類可以作為顯示終止方法模式的例子(InputStream,OutputStream,Timer和java.sql.Connection ),這四個類有著當他們的終止方法不起作用或者沒有調(diào)用的時候起到安全網(wǎng)作用的finalizer,不幸運的是這些finalizers并不會log一些警告,這樣的警告在這個API發(fā)布后不能被通用地添加,對于程序員比較不爽

第二種對于finalizer合法的使用就是關(guān)于本地對等體(Native Peers)的,一個native peer就是一個native 對象通過native方法來代表一個普通的對象,由于這個native peer并不是普通的對象,垃圾回收器當Java peer回收的時候不知道也不會去回收它,一個finalizer對于完成這個任務是合適的,假定這個native peer沒有嚴格的資源的話。如果native peer有著必須要被及時終止的資源的話,這個類應該使用一個顯式的終止方法,正如上面所描述的那樣。只要一旦需要釋放這些嚴格的資源的話終止方法就應該執(zhí)行。當然這個顯式的終止方法也可以是一個本地方法,或者是可以被本地方法調(diào)用的

需要注意的一點就是”finalizer chaining(鏈接)“并不會自動地表現(xiàn)。如果一個類有一個finalizer并且一個子類重寫了這個finalizer,這個子類必須人為手動地調(diào)用父類地finalizer,你應該在子類的try塊里面去終結(jié)在finally里面調(diào)用父類的finalizer,來保證父類的finalizer也能夠執(zhí)行,即使子類的finalization里面拋出了一個異常,反之亦然。下面給出相應的代碼塊

// Manual finalizer chaining
@Override protected void finalize() throws Throwable {
try {
     ... // Finalize subclass state
} finally {
super.finalize();
}
}

如果一個子類繼承于一個父類,并且重寫父類的finalizer,但是忘記調(diào)用了,父類的finalizer就永遠不會被調(diào)用。對于預防這樣的一個粗心而且惡意的子類是有可能的,你只需要為所有要被finalize的對象創(chuàng)建一個額外的對象。并不是去放一個finalizer在需要finalization的類里面,而是把這個finalizer放到一個匿名類里面,這個匿名類的主要任務就是finalize它的封裝的實例。匿名類的一個單一的實例,叫做finalizer guardian,每當創(chuàng)建一個封裝類的實例的時候就會創(chuàng)建出來。這個封裝的實例儲存對它的finalizer guardian的主引用在一個private的域里面,故finalizer guardian作為一個封裝實例來說對于同時的finalization可以變得可選。當這個guardian被終結(jié)的時候,它就會執(zhí)行封裝實例所需的finalization的活動,就像finalizer就是一個在這個封裝實例里面的方法

// Finalizer Guardian idiom
public class Foo {
// Sole purpose of this object is to finalize outer Foo object
private final Object finalizerGuardian = new Object() {
@Override protected void finalize() throws Throwable {
     ... // Finalize outer Foo object
}
};
... // Remainder omitted
}

需要說明的一點是,給出的Foo類沒有finalizer(繼承于Object會有一個不重要的finalizer,在這里可以忽略),所以對于一個子類的finalizer有沒有調(diào)用父類的finalizer的方法已經(jīng)變得無關(guān)緊要了,這種技術(shù)對于每一個非final的擁有一個finalizer的public類都應該被考慮一下

總結(jié)一下,不要使用finalizers除非你想構(gòu)建一個安全網(wǎng)或者是終止非嚴格的本地資源。當你使用finalizer在那些罕見的實例之中,記得調(diào)用父類的finalize。如果你想當成安全網(wǎng)來使用的話,記得去log那些對于finalizer無效的使用。最后,使用finalizer guardian,可以使finalization能夠在子類沒有或者忘記調(diào)用父類的finalize的情況依然能夠執(zhí)行。

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

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