文章作者:Tyan
博客:noahsnail.com | CSDN | 簡(jiǎn)書(shū)
Item 7: 避免使用finalizers(終結(jié)方法霍殴,Java模擬C++的析構(gòu)函數(shù))
終結(jié)方法通常是不可預(yù)測(cè)的普办,經(jīng)常是危險(xiǎn)的座硕,一般來(lái)說(shuō)是沒(méi)必要的。使用它們會(huì)引起不穩(wěn)定的行為拆檬,性能變低栓始,可移植性問(wèn)題等。終結(jié)方法有一些有效的使用硼身,這個(gè)在本條目的后面會(huì)講到硅急,但根據(jù)經(jīng)驗(yàn),你應(yīng)該避免使用終結(jié)方法佳遂。
C++程序員被警告說(shuō)不要去想像Java中模擬C++析構(gòu)函數(shù)那樣的終結(jié)方法营袜。在C++中,析構(gòu)函數(shù)是一種正吵笞铮回收對(duì)象資源的方式荚板,是構(gòu)造函數(shù)的必要對(duì)應(yīng)。在Java中吩屹,當(dāng)對(duì)象不可訪問(wèn)時(shí)啸驯,垃圾回收器會(huì)回收對(duì)象的相關(guān)資源,不需要程序員進(jìn)行專門的工作祟峦。C++析構(gòu)函數(shù)也用來(lái)回收其它的非內(nèi)存資源。在Java中徙鱼,try-finally
塊用來(lái)完成這樣的功能宅楞。
終結(jié)方法的一個(gè)缺點(diǎn)是不能保證它們及時(shí)的執(zhí)行[JLS,12.6]袱吆。從對(duì)象變得不可訪問(wèn)開(kāi)始到它的終結(jié)方法被執(zhí)行結(jié)束厌衙,這中間的時(shí)間可以任意長(zhǎng)。這意味著你不應(yīng)該在終結(jié)方法中做任何時(shí)間為關(guān)鍵的事情绞绒。例如婶希,依賴終結(jié)方法來(lái)關(guān)閉文件是一個(gè)嚴(yán)重的錯(cuò)誤,因?yàn)殚_(kāi)放的文件描述符是一種有限的資源蓬衡。如果許多文件都是打開(kāi)狀態(tài)喻杈,由于JVM執(zhí)行終結(jié)方法時(shí)是遲緩的,因此程序可能失敗狰晚,因?yàn)樗荒茉俅蜷_(kāi)文件筒饰。
盡快執(zhí)行終結(jié)方法是垃圾回收算法的主要功能,在不同的JVM實(shí)現(xiàn)中變化很大壁晒。依賴終結(jié)方法執(zhí)行及時(shí)性的程序同樣變化很大瓷们。一個(gè)程序在測(cè)試它的JVM上運(yùn)行非常完美,但在你最重要客戶支持的JVM上它卻糟糕地運(yùn)行失敗了秒咐,這是完全有可能的谬晕。
遲緩終結(jié)不僅僅是一個(gè)理論問(wèn)題。在很少的情況下携取,為一個(gè)類提供終結(jié)方法可能會(huì)隨意地延遲它實(shí)例的回收攒钳。有個(gè)同事調(diào)試一個(gè)長(zhǎng)期運(yùn)行的GUI應(yīng)用,程序莫名其妙的死掉了歹茶,拋出了OutOfMemoryError
錯(cuò)誤夕玩。分析表明在程序死亡時(shí)你弦,應(yīng)用中的終結(jié)方法隊(duì)列中有成千上萬(wàn)的圖形對(duì)象在等待被終結(jié)并回收。遺憾的是燎孟,終結(jié)方法線程的運(yùn)行優(yōu)先級(jí)要低于另一個(gè)應(yīng)用線程禽作,因此在另一個(gè)應(yīng)用線程中的對(duì)象變得可以被終結(jié)時(shí),它們不能被終結(jié)揩页。語(yǔ)言規(guī)范不能保證哪一個(gè)線程來(lái)執(zhí)行終結(jié)方法旷偿,因此沒(méi)有輕便的方式來(lái)阻止這種問(wèn)題的發(fā)生戏自,除非避免使用終結(jié)方法烁落。
不僅語(yǔ)言規(guī)范不能保證終結(jié)方法及時(shí)的執(zhí)行;而且也不能保證終結(jié)方法得到執(zhí)行剧董。這完全有可能兔仰,甚至有可能一個(gè)程序終止時(shí)茫负,一些不能訪問(wèn)的對(duì)象的終結(jié)方法都沒(méi)有執(zhí)行。結(jié)論就是:你從不該依賴終結(jié)方法來(lái)更新重要的持續(xù)狀態(tài)乎赴。例如忍法,依賴一個(gè)終結(jié)方法來(lái)釋放一個(gè)共享資源,例如數(shù)據(jù)庫(kù)榕吼,的持續(xù)鎖饿序,很容易引起整個(gè)分布式系統(tǒng)當(dāng)?shù)簟?/p>
不要被System.gc
和System.runFinalization
方法誘惑。它們可能會(huì)增加終結(jié)方法得到執(zhí)行的幾率羹蚣,但它們不能保證它原探。能保證終結(jié)方法執(zhí)行的唯一方法是System.runFinalizersOnExit
以及它臭名昭著的孿生兄弟Runtime.runFinalizersOnExit
。這些方法都有致命的缺陷并且已經(jīng)被廢棄了[ThreadStop]顽素。
以防你還不相信終結(jié)方法應(yīng)該被避免咽弦,這兒有另一個(gè)情況值得思考:如果在終結(jié)方法執(zhí)行期間拋出了一個(gè)無(wú)法捕獲的異常,這個(gè)異常被忽略了戈抄,對(duì)象的終結(jié)方法終止了[JLS离唬,12.6]。不能捕獲的異郴耄可能會(huì)使對(duì)象處于崩潰狀態(tài)输莺。如果另一個(gè)線程試圖使用這樣一個(gè)崩潰的對(duì)象,任何不確定性的行為都有可能發(fā)送裸诽。通常嫂用,一個(gè)未被捕獲的異常會(huì)終止線程并打印棧軌跡,但如果它發(fā)生在一個(gè)終結(jié)方法中丈冬,將不會(huì)打印出警告嘱函。
哦,還有一件事:使用終結(jié)方法會(huì)有嚴(yán)重的性能問(wèn)題埂蕊。在我的機(jī)器上往弓,創(chuàng)建并銷毀一個(gè)簡(jiǎn)單對(duì)象大約是5.6納秒疏唾。添加一個(gè)終結(jié)方法會(huì)將這個(gè)時(shí)間增加到2400納秒。換句話說(shuō)函似,創(chuàng)建一個(gè)對(duì)象并用終結(jié)方法銷毀對(duì)象比正常情況下大約慢了430倍槐脏。
因此當(dāng)一個(gè)類的對(duì)象封裝的資源需要結(jié)束時(shí),你應(yīng)該用什么來(lái)代替一個(gè)類的終結(jié)方法撇寞?例如文件或線程顿天?提供一個(gè)顯式的結(jié)束方法,當(dāng)類的實(shí)例不再需要時(shí)蔑担,要求類的客戶端在每個(gè)實(shí)例上都調(diào)用這個(gè)方法牌废。一個(gè)值得提及的細(xì)節(jié)是,實(shí)例必須跟蹤它是否已經(jīng)被終結(jié):顯式的終結(jié)方法必須記錄在一個(gè)私有字段中啤握,這個(gè)字段表明對(duì)象不再有效鸟缕,如果其它方法再對(duì)象終結(jié)后調(diào)用對(duì)象,其它方法必須檢查這個(gè)字段并拋出IllegalStateException
排抬。
顯式結(jié)束方法的典型例子是InputStream
叁扫,OutputStream
和java.sql.Connection
的關(guān)閉方法。另一個(gè)例子是java.util.Timer
的cancel
方法畜埋,它會(huì)進(jìn)行必要的狀態(tài)檢查并一起線程相關(guān)的Timer
實(shí)例平穩(wěn)的結(jié)束它自己。java.awt
的例子包括Graphics.dispose
和Window.dispose
畴蒲。這些方法經(jīng)常被忽視悠鞍,可以預(yù)料會(huì)引起可怕的性能后果。一個(gè)相關(guān)的方法是Image.flush
模燥,它會(huì)釋放所有Image
實(shí)例相關(guān)的資源咖祭,但會(huì)將實(shí)例保持在仍可用的狀態(tài),如果必要的時(shí)候重新分配資源蔫骂。
顯式結(jié)束方法通過(guò)與try-finally
結(jié)構(gòu)結(jié)合來(lái)確保終結(jié)么翰。在finally
語(yǔ)句塊的內(nèi)部調(diào)用顯式的結(jié)束方法來(lái)確保它得到執(zhí)行,即使對(duì)象使用時(shí)拋出了一個(gè)異常:
// 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
}
那終結(jié)方法有什么好處呢辽旋?有兩種可能的合法應(yīng)用浩嫌。一個(gè)是作為『安全網(wǎng)』,以防對(duì)象擁有者忘記調(diào)用它的顯式結(jié)束方法补胚。但這不能保證終結(jié)方法得到及時(shí)的調(diào)用码耐,當(dāng)客戶端調(diào)用顯式結(jié)束方法失敗時(shí),在那種情況下(希望很少)溶其,后面釋放資源總比不釋放資源要好骚腥。但終結(jié)方法如果發(fā)現(xiàn)資源仍沒(méi)有被釋放,它應(yīng)該輸出一個(gè)警告瓶逃,因?yàn)檫@意味著客戶端代碼存在一個(gè)BUG束铭,它應(yīng)該被修正廓块。如果你正在考慮寫這樣一個(gè)安全網(wǎng)終結(jié)方法,要仔細(xì)思考這種額外的保護(hù)是否值得額外的代價(jià)契沫。
作為顯式結(jié)束方法模式引用的四個(gè)例子(FileInputStream
带猴,FileOutputStream
,Timer
和Connection
)都有終結(jié)方法作為安全網(wǎng)以防它們的結(jié)束方法沒(méi)有被調(diào)用埠褪。遺憾的是這些終結(jié)方法不輸出警告浓利。這種警告通常在API發(fā)布后不能進(jìn)行添加,因?yàn)樗鼤?huì)損壞現(xiàn)有的客戶端钞速。
終結(jié)方法的第二個(gè)合法使用是關(guān)于對(duì)象的本地對(duì)等體贷掖。本地對(duì)等體是一個(gè)本地對(duì)象,普通對(duì)象通過(guò)本地方法委托給本地對(duì)象渴语。由于本地對(duì)等體不是一個(gè)正常的對(duì)象苹威,當(dāng)它的Java對(duì)等體回收時(shí),垃圾回收器不知道并且不能回收它驾凶。假設(shè)本地對(duì)等體不擁有重要的資源牙甫,終結(jié)方法是執(zhí)行這個(gè)任務(wù)的合適工具。如果本地對(duì)等體擁有必須及時(shí)終止的資源调违,這個(gè)類應(yīng)該有一個(gè)顯式的結(jié)束方法窟哺,如上所述。結(jié)束方法應(yīng)該用來(lái)釋放重要資源技肩。結(jié)束方法可以是一個(gè)本地方法或它可以調(diào)用一個(gè)本地方法且轨。
很重要的一點(diǎn)就是要注意『終結(jié)方法鏈』是不能自動(dòng)執(zhí)行的。如果一個(gè)類(不是Object
)有一個(gè)終結(jié)方法虚婿,一個(gè)子類覆寫了它旋奢,子類終結(jié)方法必須手動(dòng)調(diào)用父類終結(jié)方法。你應(yīng)該try
塊內(nèi)終止這個(gè)子類并在對(duì)應(yīng)的finally
塊調(diào)用父類終結(jié)方法然痊。這保證了父類終結(jié)方法得到了執(zhí)行至朗,即使子類終結(jié)方法拋出異常,反之亦然剧浸。下面是它的一個(gè)例子锹引、注意這個(gè)例子使用了Override
注解(@Override
),在release 1.5版本中添加∷粝悖現(xiàn)在你可以忽略Override
注解粤蝎,或看Item 36弄明白它是什么意思:
// Manual finalizer chaining
@Override
protected void finalize() throws Throwable {
try {
... // Finalize subclass state
} finally {
super.finalize();
}
}
// 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
沒(méi)有終結(jié)方法(除非它從Object
繼承一個(gè)無(wú)關(guān)緊要的),因此子類的終結(jié)方法是否調(diào)用super.finalize
是不重要的袋马。每一個(gè)含有終結(jié)方法的非終結(jié)公有類都應(yīng)該考慮這個(gè)技術(shù)初澎。
總結(jié):不要使用終結(jié)方法,除非是用作安全網(wǎng)或用來(lái)終止一個(gè)非重要的本地資源。在那些你使用終結(jié)方法的稀少實(shí)例中碑宴,記住調(diào)用super.finalize
软啼。如果你使用終結(jié)方法作為安全網(wǎng),記住在終結(jié)方法中輸出非法用法延柠。最后祸挪,如果你需要將終結(jié)方法關(guān)聯(lián)到一個(gè)公有的,非終結(jié)類贞间,考慮使用終結(jié)方法守護(hù)者贿条,即使子類終結(jié)方法調(diào)用super.finalize
失敗,也會(huì)進(jìn)行終結(jié)增热。