我的看法
優先使用try-with-resource來對重要資源使用完畢之後進行回收是一個好習慣,使用finalizers和cleaners是很危險的,因為:結果不确定而且性能損耗大。他兩隻能在有限的場景下考慮使用,但是也要特别注意這個不确定性和性能損失。
快速記憶
翻譯
這裡約定finalizers=回收器,cleaners=清理器。
回收器是不可預測的,通常是很危險的,并且沒有必要。 使用可能帶來不穩定的行為和更差的性能,并且可能傳播或者轉移這種問題。回收器有一些合法的使用場景,後面會在這條規則中包含,但是原則上,你應該避免使用它,對Java9來說,回收器已經不建議使用了,但是卻仍然被java的庫使用,Java9替換回收器(finalizers)的是清理器(cleaners) ,清理器比回收器危險性小,但是仍然不可預測,仍然很慢,仍然沒有必要 。
C++程式員被告誡不要去思考銷毀器,就像Java語言中模拟C++的一樣的回收器和清理器,在C++中,銷毀器是回收對象關聯資源的正常方法,是跟構造器一樣必要的元件,在java語言中,當對象變得不可達的時候,垃圾回收器(garbage collector)會回收對象相關的存儲空間,程式員無需花費特别的關注。C++的銷毀器也被用來回收另外的非記憶體資源。在java語言中,使用 try-with-resources 或者 try-finally 語句塊也是為了回收非記憶體資源的目的。
回收器和清理器的一個缺點是無法保證他們被及時的執行。在對象變為不可達到的時候回收器或者清理器運作起來可能花費不可預計的時間;這個意味着 你必須不能在回收器和清理器中做任何時間敏感的事情 ,舉個例子,依賴清理器或者回收器去關閉檔案流是一個天大的錯誤,因為檔案的描述符是有限的資源,如果大量的檔案因為系統的拖拉或者延遲運作回收器和清理器導緻大量被打開,一個程式會因為無法再打開檔案而運作失敗。
及時的執行回收和清理是垃圾回收算法的核心功能,這個功能實作方式不同對及時執行機率差别很大。依賴回收器和清理器的及時執行來影響程式的行為結果同樣差别很大。一個程式在你的測試JVM上跑的很完美而在你的一個最重要的優質客戶那裡不幸的失敗是完全可能的。延遲回收不僅僅是一個假設的問題,為一個類提供一個回收器很可能随時延遲它的執行個體的回收,一個同僚調試一個長時間運作的GUI程式神秘的被一個記憶體洩漏弄死了,分析披露了程式的死亡時間,這個程式有上千個圖形對象在回收器隊列中等着被回收。很不幸的是,回收器線程的運作優先級比另外一個應用内的線程低,是以對象無法在有資格回收的節奏下被回收。Java語言明确無法保證哪個線程将要被回收器回收,是以,沒有一個等同的方法來避免這類問題,還不如忍住不使用回收器。
考慮到作者可以控制自己的清理線程,清理器比回收器好一些。但是清理器仍然跑在後端,在垃圾回收器的掌控之下,是以也沒有任何保證會被及時調用,不僅僅是Java語言規約無法保證及時執行回收和清理,也無法保證他們最後會不會執行。在一些不可達的對象上程式中斷了并沒有執行清理器和回收器是很有可能的。基于這個結果,你不應該依賴回收器和清理器去更新持久化的狀态 ,舉個例子:依賴回收器或者清理器去釋放一個在共享資源(比如說資料庫上的持續存在的鎖)是一個很好的方法讓你的整個分布式系統完全停止。
不要被
和
System.gc()
引誘,這可能會提高回收器和清理器執行的機率,但是并不一定保證。這兩個方法一緻保證聲稱:
System.runFinalization()
和它的惡魔雙胞胎
System.runfinalizersOnExit()
Runtime.runFinalizersOnExit()
, 這兩個方法有緻命的瑕疵,并且已經被廢棄十年了。
回收器的另外一個問題是在回收期間引起的異常是被忽略的,并且對象的回收被終止。無法捕獲的異常可能讓其他的對象處于錯誤的狀态。如果有另外一個線程嘗試使用這樣一個處于錯誤狀态的對象,可能産生任意不确定的行為。通常,一個不被捕獲的異常會中斷線程并列印堆棧資訊,但是前提是不是發生在回收器中,它甚至不會列印一個警告。清理器不會有這樣的問題,因為使用清理器的庫控制自己的線程。
使用回收器和清理器有嚴重的性能問題 ,在我的機器上,建立一個自動關閉(AutoCloseable)的獨享,使用try-with-resource的方式,當它被垃圾回收的時候耗費12納秒,使用回收器替換,時間增加到550納秒,換句話來說,使用回收器來建立和回收對象慢了50倍,這主要是因為回收器減低了垃圾回收器的效率。
如果使用清理器來清理類的所有執行個體,清理器比回收器相比較來說更快一些(在我的機器上大概500納秒一個執行個體),但是如果你在上文提到的安全的網絡中使用清理器更快,在這些情況下,我的機器上,建立,清理,銷毀一個對象耗費66納秒,這意味着如果你不使用清理器你需要5倍(不是50倍)關注安全網絡的的可靠性。
清理器有一個嚴重的安全問題,它大開你的類,遭受回收器的攻擊 ,回收器攻擊後面的原因也很簡單:如果一個異常從構造函數或者序列化(
和
readObject
readResolve
方法 )抛出,運作在這個部分執行個體化的對象的一個惡意子類的回收器會死在藤蔓上。 這個回收器會記錄靜态成員的對象引用,防止它被垃圾回收。當這個畸形的對象被記錄下來,在第一個地方,用這個不允許存在的畸形對象調用任意方法是小事一樁。
構造函數中抛出一個異常防止對象存在是綽綽有餘的,但是對于回收器來說不是這樣的。 這樣的攻擊有可怕的後果,Final類可以免疫回收器攻擊因為不能建立惡意子類,為了保護回收器攻擊非Final類,寫一個final的回收器方法沒啥鳥用 ,是以,在一個類的對象資源需要中斷,比如檔案或者線程,你應該用什麼東西去替代回收器和清理器呢?
讓你的類實作AutoCloseable接口,讓調用該類的用戶端在不需要執行個體的時候調用close方法即可 , 通常也可以使用try-with-resource 來保證終止,即使在面對異常的場景。 另外一個值得提起的細節是執行個體必須關注執行個體是否被關閉掉了,close方法必須記錄在成員變量上,對象不再有效,别的方法必須檢查這個成員變量,當這個對象被關閉的時候去調用必須抛出
IllegalStateException
。
是以,有哪些場景适合回收器和清理器呢? 他們或許有兩種合法的用處。
一個是位于一個安全網絡,資源的擁有者疏忽了調用它的close方法,盡管沒法保證回收器和清理器會不會及時的執行,稍後釋放資源比如果用戶端失敗從來不釋放要更好,如果你在考慮寫一個這樣的安全網絡回收器,長遠的想一想這個保護是否值得,有一些java的工具類,比如
FileInputStream,FileOutputStream,ThreadPoolExecutor,java.sql.Connection
為了安全網絡的原因,有回收器。
另外一個合理使用清理器是考慮到本地同位對象,一個本地同位對象是一個(普通的對象通過本地方法委托)本地的非Java對象,垃圾收集器不知道它,當java同位對象回收的時候,它不會回收,假設性能是可以接受的,本地同位對象持有非關鍵資源,回收器或者清理器也許是這個任務【回收資源】的一個比較好的手段;如果性能無法接受,或者本地同位對象持有的資源必須及時的回收,如前描述,這個類必須有一個close方法。
清理器使用起來有一點困難,下面是一個Room類,展示了這個事實。讓我們假設rooms必須被清理在他們回收之前,Room類實作了AutoCloseable接口,使用清理器自動清理安全網絡的事實隻是一個實作細節。不像回收器,清理器不會污染公共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清理房間的資源,在這個場景下,它是一個簡單的numJunkPiiles成員,代表了房間的混亂度,更切合實際的是,它也許是一個final的long包含了一個本地同位對象的指針,State實作了Runable,它的run方法至多被調用一次,當我們在Room的構造函數中使用cleaner注冊State執行個體,我們拿到Cleanable,run方法會被兩個中的一個條件觸發:通常被Room的close方法調用Cleanable的clean方法觸發,如果用戶端調用close方法的時候垃圾回收器有資格回收Room執行個體導緻失敗,清理器會調用State的run方法。
一個State執行個體沒有引用Room執行個體這個很重要,如果存在引用,會建立一個循環,導緻Room執行個體無法被垃圾收集器回收(并且别自動清理),是以,State必須是一個靜态的内嵌類,因為非靜态的内嵌類包含他們環繞執行個體的引用,使用lambda也是不明智的,因為他們可以很輕松的占據诶呦關閉對象的引用。
如前所說,Room的cleaner隻在安全網絡下使用,如果用戶端使用try-with-resoure塊包圍Room執行個體,自動清理是用不上的,良好行為的用戶端類似這樣:
如你所期待的那樣,運作Adult程式列印Goodbye,伴随着Cleaning Room ,但是病态行為從來不清理房間的程式應該是怎樣的?public class Adult { public static void main(String[] args) { try (Room myRoom = new Room(7)) { System.out.println("Goodbye"); } } }
public class Teenager { public static void main(String[] args) { new Room(99); System.out.println("Peace out"); } }
你也許會期待列印Peace out , 伴随着Cleaning room,但是在我的機器上,它從來不列印Cleaning Room , 隻是結束,這是我們之前提到的不确定性,清理器專區提到:在System.exit期間清理器的行為随着實作的不同而不同。無法保證關聯的清理動作是否會調用。盡管專區沒有提到,正常程式結束的時候結果也是一樣。添加一行 System.gc() 到Teenager的main方法足夠是的它列印Cleaning room 在它結束之前,但是無法保證在你的機器上看到相同的行為結果。
總結:不要使用清理器,回收器,即使是在安全網絡下中斷不重要的本地資源,也要意識到不确定性和性能的影響。
原創不易,轉載請注明出處,一起學習Effective java 3,提高代碼品質,程式設計技能。歡迎一起讨論。