??Finalizers是不可預(yù)測(cè)的,通常是危險(xiǎn)的,一般來說是沒有必要的。它們的使用可能會(huì)造成不穩(wěn)定的行為,不佳的性能和可移植性問題。Finalizers有一些有效的用途,在本條目將會(huì)介紹,但是作為一個(gè)規(guī)則,你應(yīng)該避免使用它們。在Java9開始,finalizers被棄用了,但是在Java庫(kù)中仍然被使用。在Java9中,finalizers的替代者是cleaners。Cleaners比f(wàn)inalizers的危險(xiǎn)性要小,但仍然不可預(yù)測(cè),緩慢且通常沒有必要。
??提醒C++程序員不要將Java的finalizers和cleaners與析構(gòu)函數(shù)作類比。在C++中,析構(gòu)函數(shù)是回收與對(duì)象關(guān)聯(lián)的資源的常用方法,是構(gòu)造函數(shù)必要的對(duì)應(yīng)面。在Java中,垃圾收集器當(dāng)它變得無(wú)法訪問時(shí)回收與對(duì)象關(guān)聯(lián)的存儲(chǔ),不需要程序員作特別處理。C++析構(gòu)函數(shù)也用于回收其他非內(nèi)存資源。在Java中,try-with-resources 或 try-finally塊是用于此目的(item9)。
??finalizers和cleaners的一個(gè)缺點(diǎn)是無(wú)法保證他們會(huì)被及時(shí)處理。對(duì)象變得不可訪問和finalizer或cleaner運(yùn)行之間可能需要任意長(zhǎng)的時(shí)間。這意味著你不應(yīng)該在finalizer或cleaner中做任何時(shí)間關(guān)鍵的事情。比如,依賴finalizer或cleaner來關(guān)閉文件是一個(gè)嚴(yán)重錯(cuò)誤,因?yàn)榇蜷_文件描述符是有限的資源。如果許多文件由于系統(tǒng)在運(yùn)行finalizers和cleaners時(shí)的延遲處于打開狀態(tài),程序可能會(huì)因?yàn)椴辉倌艽蜷_文件而失敗。
??finalizers和cleaners的執(zhí)行速度首要取決于垃圾回收算法,不同的垃圾回收算法在不同實(shí)現(xiàn)中變化很大。取決于finalizer和cleaner的執(zhí)行速度的程序的行為同樣可能變化。
所以完全有可能你的程序在你測(cè)試的JVM上完美運(yùn)行,但是在你最重要的客戶上非常不幸地運(yùn)行失敗。
??緩慢的finalization不只是一個(gè)理論問題。為一個(gè)類提供finalizer可以任意延遲其實(shí)例的回收。一位同事調(diào)試一個(gè)長(zhǎng)時(shí)間運(yùn)行的GUI應(yīng)用程序,該程序因?yàn)镺utOfMemoryError神秘地死亡。分析顯示,在它死亡時(shí)。應(yīng)用程序在finalizer隊(duì)列上有數(shù)以千計(jì)的圖形對(duì)象,等待被銷毀和回收。不幸的是,finalizer線程的運(yùn)行優(yōu)先級(jí)低于其他應(yīng)用程序線程,因此對(duì)象沒有及時(shí)被銷毀。語(yǔ)言規(guī)范不保證哪個(gè)線程將執(zhí)行finalizers,因此沒有可移植的方法來阻止這些問題,除了避免使用finalizers。Cleaners在這一方面要比f(wàn)inalizers好一些,因?yàn)轭愖髡呖梢钥刂扑麄冏约旱腸leaner線程,但是cleaner仍然在后臺(tái)運(yùn)行,在垃圾回收器的控制之下,所以不能保證及時(shí)清理。
??該規(guī)范不僅不能保證finalizers或cleaners可以及時(shí)運(yùn)行;而且它一點(diǎn)也不保證它們會(huì)運(yùn)行。這完全有可能。甚至可能在程序終止時(shí),某些對(duì)象不再可訪問時(shí),還沒有運(yùn)行。因此,你應(yīng)該絕不依賴finalizer或cleaner來更新持久狀態(tài)。例如,依賴于finalizer或cleaner來釋放一個(gè)在共享資源上(如數(shù)據(jù)庫(kù))的持久鎖是將你整個(gè)分布式系統(tǒng)停止運(yùn)行的好方法。
??不要被System.gc和System.runFinalization方法迷惑。它們可能會(huì)增加finalizers和cleaners被執(zhí)行的記錄,但是它們并不能保證。有兩種方法曾經(jīng)聲明提供這個(gè)保證:System.runFinalizersOnExit 和它邪惡的雙胞胎,Runtime.runFinalizersOnExit。這些方法有著致命缺陷并且被棄用數(shù)十年了。
??finalizers的另一個(gè)問題是在finalization期間被忽略時(shí)會(huì)拋出未捕獲的異常,終止對(duì)象的終結(jié)(finalization)。未捕獲的異常會(huì)導(dǎo)致其他對(duì)象處于損壞的狀態(tài)。如果另一個(gè)線程嘗試使用這樣損壞的對(duì)象,可能會(huì)造成任意非確定性行為。通常來說,一個(gè)未捕獲的異常將終止線程并打印堆棧跟蹤,但是發(fā)生在finalizer中就不會(huì)--它甚至不會(huì)打印警告。Cleaners沒有這個(gè)問題因?yàn)槭褂胏leaner的庫(kù)可以控制它的線程。
??使用finalizers和cleaners是會(huì)嚴(yán)重影響性能。在我機(jī)器上,創(chuàng)建一個(gè)簡(jiǎn)單的AutoCloseable對(duì)象,使用using try-with-resources關(guān)閉它,并讓垃圾回收器回收它花費(fèi)的時(shí)間為12ns。使用finalizer將時(shí)間增加到550ns。換句話說,使用finalizers創(chuàng)建和銷毀對(duì)象的速度要慢50倍。這主要是因?yàn)閒inalizers抑制了高效的垃圾回收。如果使用cleaner來清除所有類的實(shí)例(在我的機(jī)器上每個(gè)實(shí)例大概500ns),它的速度和finalizer相當(dāng)。但是你僅將它們用作安全網(wǎng),cleaner將會(huì)快很多。如下所述,在這些情況下,在我的機(jī)器上創(chuàng)建,清理和銷毀一個(gè)對(duì)象花費(fèi)了66ns,這意味著如果你不使用安全網(wǎng),你需要支付5倍(而不是50倍)的保險(xiǎn)費(fèi)。
??Finalizers有一個(gè)嚴(yán)重的問題:它們打開你的類受到finalizer攻擊。finalizer攻擊背后的想法很簡(jiǎn)單:如果一個(gè)異常在構(gòu)造方法或它的序列化等價(jià)物-readObject和readResolve方法(12章)被拋出-惡意子類的finalizer可以運(yùn)行在部分”胎死腹中“的構(gòu)造的對(duì)象上運(yùn)行。finalizer可以在靜態(tài)字段中記錄對(duì)象的引用,防止被垃圾回收。一旦異常的對(duì)象被記錄了,在這個(gè)對(duì)象上調(diào)用任意方法是一件簡(jiǎn)單的事情,這些方法本來就不應(yīng)該被允許存在。從構(gòu)造方法中拋出異常應(yīng)該足以防止對(duì)象存在;在finalizer面前,并非如此。如此攻擊會(huì)產(chǎn)生可怕的后果。final類免于finalizer攻擊因?yàn)闆]有人可以編寫final類的惡意子類。為了保護(hù)非final類免受finalizer攻擊,請(qǐng)編寫一個(gè)final finalize方法不執(zhí)行任何操作。
??除了為一個(gè)對(duì)象封裝需要終止資源的類(例如文件或線程)編寫finalizer或cleaner,你應(yīng)該怎么做呢?只需要將你的類實(shí)現(xiàn)AutoCloseable,并要求其客戶端在不再需要時(shí)調(diào)用每個(gè)實(shí)例上的close方法,通常使用 try-with-resources確保終止,即使在面對(duì)異常的情況下 (item9)。一個(gè)值得提到的細(xì)節(jié)是實(shí)例必須追蹤它是否被關(guān)閉:close方法必須在對(duì)象不再有效的字段上記錄,并且一旦該對(duì)象被關(guān)閉后調(diào)用,其他方法必須檢查這個(gè)字段并在調(diào)用之后拋出IllegalStateException。
??那么,cleaner和finalizer有好的地方嗎?它們可能有兩個(gè)合理的應(yīng)用。其一是在資源的擁有者忽略了調(diào)用它的close方法時(shí),它可以充當(dāng)安全保障的角色。雖然不能保證cleaner和finalizer會(huì)立即運(yùn)行(或根本不允許),但很遲釋放資源總比客戶端沒有這么做要更好。如果你考慮編寫這樣類似finalizer的安全保障,請(qǐng)仔細(xì)考慮這樣的保護(hù)的代價(jià)是否值得。某些Java類庫(kù),比如FileInputStream,FileOutputStream, ThreadPoolExecutor和 java.sql.Connection,就有這樣的安全保障的finalizer。
??cleaners的第二個(gè)合理用途是與native peers相關(guān)。native peer是一個(gè)native(非Java)對(duì)象,這種對(duì)象是由普通對(duì)象委托native方法生成。因?yàn)橐粋€(gè)native peer不是普通對(duì)象,所以它對(duì)象的Java對(duì)象回收時(shí)垃圾回收器不認(rèn)識(shí)它也不回收它。cleaner和finalizer可能就是這個(gè)任務(wù)適合的工具,假設(shè)性能是可接受的并且native peer沒有保留關(guān)鍵資源。如果性能不可接受或native peer有必須被立即回收的資源,該類應(yīng)該有close方法,如前所述。
??Cleaner用起來有點(diǎn)棘手。下面是一個(gè)簡(jiǎn)單的房間類展示的用法。我們假設(shè)房間在回收前必須被清潔。房間類實(shí)現(xiàn)了AutoCloseable;使用cleaner自動(dòng)清理只是實(shí)現(xiàn)的細(xì)節(jié)。不像finalizer,cleaner不污染類的公共API。

??靜態(tài)內(nèi)部類State包含著需要被cleaner清理房間的資源。在這種情況下,它只是numJunkPiles字段,這個(gè)字段代表了房間混亂的程度。更實(shí)際地說,它可能是一個(gè)包含native peer的指針的final字段。state實(shí)現(xiàn)了Runnable,它的run方法最多被調(diào)用一次,當(dāng)我們?cè)趓oom構(gòu)造方法中使用cleaner注冊(cè)State實(shí)例時(shí)得到了Cleanable。run方法的調(diào)用將會(huì)被下列兩種方法之一觸發(fā):通常它通過room的close方法來調(diào)用cleanable的clean方法被觸發(fā)。如果客戶端無(wú)法在room實(shí)例符合垃圾回收條件下調(diào)用close方法,cleaner(最好)會(huì)調(diào)用state的run方法。
??state實(shí)例不引用它的room實(shí)例是重要的。如果引用了,這將創(chuàng)建一個(gè)循環(huán),將阻止room實(shí)例被垃圾回收(以及被自動(dòng)清理)。所以,state必須是一個(gè)靜態(tài)內(nèi)部類,因?yàn)榉庆o態(tài)內(nèi)部類包含了對(duì)其封閉實(shí)例的引用(item24),使用lambda同樣不可取,因?yàn)樗鼈兛梢暂p易捕獲封閉對(duì)象的引用。
??正如我們之前所述,room的cleaner只用作安全保障。如果客戶端在try-with-resource 塊中圍繞所有room實(shí)例,自動(dòng)清理將再也不需要。這個(gè)良好表現(xiàn)的客戶端演示了這種行為:

??正如你所期望的,允許Adult程序會(huì)打印GoodBye,隨后room被清理。但是下列的不良行為的程序,從不clean它的room呢?

??你可能會(huì)期望它將打印Peace out,并清理room,但是在我的機(jī)器上,它從不打印清理room的信息,它只是退出了。這就是之前談到的不可預(yù)知性。cleaner規(guī)范說,“在System.exit期間cleaner的行為是特定的實(shí)現(xiàn),不保證是否調(diào)用清理行為?!彪m然規(guī)范沒有明說,但是正常程序退出也是一樣的。在我的機(jī)器上,在Teenager的main方法中加入System.gc()足以讓它在退出之前打印清理room的信息,但是這不保證在你的機(jī)器上也有相同行為。
??總結(jié),不要使用cleaner和finalizer,在java9之前的版本也不行,除非作為一個(gè)安全保障或終止非關(guān)鍵本地資源。即使是這樣,小心不確定性和性能后果。
本文寫于2019.1.13,歷時(shí)11天