垃圾收集幾乎是每個開發人員都喜愛的一個 Java 平台特性,它簡化了開發,消除了所有種類的潛在代碼錯誤。可盡管垃圾收集一般來說可以讓您無需進行資源治理,有時候您還是必須自己進行一些內務處理。在本文中,Brian Goetz 討論了垃圾收集的局限性,並指出了您必須自己做內務處理的場景。
小時候,父母總是叮囑我們玩了玩具之後要收好。假如您仔細想想,其實這種唠叨並不過分,要保持整潔是因為存在實際的限制,房間裡沒有太多的空間,假如到處堆滿了玩具,那麼連走路都無處下腳了。
假如有了足夠的空間,保持整潔就不是那麼必要了。空間越多,就越不必要保持整潔。Arlo Guthrie 聞名的民謠 Alice's Restaurant Massacre 說明了這一點:
他們住在教堂樓下的大廳,裡面的椅子全都搬走了,剩下一個空蕩蕩的大房間,所以他們想,很長時間都不用把垃圾扔出去,有的是地方裝垃圾……
無論如何,垃圾收集可以幫我們減輕內務整理方面的工作。
顯式地釋放資源 Java 程序中使用的絕大多數資源都是對象,垃圾收集在清理對象方面做得很好。因此,您可以使用任意多的 String。垃圾收集器最終無需您的干預就會算出它們何時失效,並收回它們使用的內存。
另一方面,像文件句柄和套接字句柄這類非內存資源必須由程序顯式地釋放,比如使用 close()、destroy()、shutdown() 或 release() 這樣的方法來釋放。有些類,比如平台類庫中的文件句柄流實現,提供終結器(finalizer)作為安全保證,以便當垃圾收集器確定程序不再使用資源而程序卻忘了釋放資源時,終結器還可以來做這個釋放工作。但是盡管文件句柄提供了終結器來在您忘記了時為您釋放資源,最好還是在使用完之後顯式地釋放資源。這樣做可以更早地釋放資源,降低了資源耗盡的可能。
對於有些資源來說,一直等到終結(finalization)釋放它們是不可取的。對於重要的資源,比如鎖獲取和信號量許可證,Lock 或 Semaphore 直到很晚都可能不會被垃圾收集掉。對於數據庫連接這樣的資源,假如您等待終結,那麼肯定會消耗完資源。許多數據庫服務器根據許可的容量,只接受一定數量的連接。假如服務器應用程序為每個請求都打開一個新的數據庫連接,然後用完之後就不管了,那麼數據庫遠遠未到終結器關閉不再需要的連接,就會到達它的最高容量。
只限於一個方法的資源 多數資源都不會持續整個應用程序的生命周期,相反,它們只被用於一個活動的生命周期。當應用程序打開一個文件句柄讀取文件以處理文檔時,它通常讀取文件後就不再需要文件句柄了。
在最簡單的情況下,資源在同一個方法調用中被獲取、使用和釋放,比如清單 1 中的 loadPropertiesBadly() 方法:
清單 1. 不正確地在一個方法中獲取、使用和釋放資源 —— 不要這樣做
public static Properties loadPropertiesBadly(String fileName)
throws IOException {
FileInputStream stream = new FileInputStream(fileName);
Properties props = new Properties();
props.load(stream);
stream.close();
return props;
}
不幸的是,這個例子存在潛在的資源洩漏。假如一切進展順利,流將會在方法返回之前被關閉。但是假如 props.load() 方法拋出一個 IOException,那麼流則不會被關閉(直到垃圾收集器運行其終結器)。解決方案是使用 try...finally 機制來確保流被關閉,而不管是否發生錯誤,如清單 2 所示:
清單 2. 正確地在一個方法中獲取、使用和釋放資源
public static Properties loadProperties(String fileName)
throws IOException {
FileInputStream stream = new FileInputStream(fileName);
try {
Properties props = new Properties();
props.load(stream);
return props;
}
finally {
stream.close();
}
}
注重,資源獲取(打開文件)是在 try 塊外面進行的;假如把它放在 try 塊中,那麼即使資源獲取拋出異常,finally 塊也會運行。不僅該方法會不適當(您無法釋放您沒有獲取的資源),finally 塊中的代碼也可能拋出其自己的異常,比如 NullPointerException。從 finally 塊拋出的異常取代導致塊退出的異常,這意味著原來的異常丟失了,不能用於幫助進行調試。 並不總像看起來那麼輕易
使用 finally 來釋放在方法中獲取的資源是可靠的,但是當涉及多個資源時,很輕易變得難以處理。下面考慮這樣一個方法,它使用一個 JDBC Connection 來執行查詢和迭代 ResultSet。該方法獲得一個 Connection,使用它來創建一個 Statement,並執行 Statement 以得到一個 ResultSet。但是中間 JDBC 對象 Statement 和 ResultSet 具有它們自己的 close() 方法,並且當您使用完之後,應該釋放這些中間對象。然而,進行資源釋放的 “明顯的” 方式並不起作用,如清單 3 所示:
清單 3. 不成功的釋放多個資源的企圖 —— 不要這樣做