垃圾收集幾乎是每一個開發人員都喜好的一個 Java™ 平臺特性,它簡化了開發,消除了全部種類的潛在代碼錯誤。可儘管垃圾收集通常來講可讓您無需進行資源管理,有時候您仍是必須本身進行一些內務處理。
java顯式地釋放資源數據庫
Java 程序中使用的絕大多數資源都是對象,垃圾收集在清理對象方面作得很好。所以,您可使用任意多的
String
。垃圾收集器最終無需您的干預就會算出它們什麼時候失效,並收回它們使用的內存。
安全另外一方面,像文件句柄和套接字句柄這類非內存資源必須由程序顯式地釋放,好比使用
close()
、destroy()
、shutdown()
或release()
這樣的方法來釋放。有些類,好比平臺類庫中的文件句柄流實現,提供終結器(finalizer)做爲安全保證,以便當垃圾收集器肯定程序再也不使用資源而程 序卻忘了釋放資源時,終結器還能夠來作這個釋放工做。可是儘管文件句柄提供了終結器來在您忘記了時爲您釋放資源,最好仍是在使用完以後顯式地釋放資源。這 樣作能夠更早地釋放資源,下降了資源耗盡的可能。
服務器對於有些資源來講,一直等到終結(finalization)釋放它們是不可取的。對於重要的資源,好比鎖獲取和信號量許可證,
Lock
或Semaphore
直到很晚均可能不會被垃圾收集掉。對於數據庫鏈接這樣的資源,若是您等待終結,那麼確定會消耗完資源。許多數據庫服務器根據許可的容量,只接受必定數量的 鏈接。若是服務器應用程序爲每一個請求都打開一個新的數據庫鏈接,而後用完以後就無論了,那麼數據庫遠遠未到終結器關閉再也不須要的鏈接,就會到達它的最高容 量。
網絡多數資源都不會持續整個應用程序的生命週期,相反,它們只被用於一個活動的生命週期。當應用程序打開一個文件句柄讀取文件以處理文檔時,它一般讀取文件後就再也不須要文件句柄了。框架
在最簡單的狀況下,資源在同一個方法調用中被獲取、使用和釋放,代碼以下所示測試
Java代碼
- //不正確地在一個方法中獲取、使用和釋放資源
—— 不要這樣作 - public
static Properties loadPropertiesBadly(String fileName) throws IOException { new FileInputStream stream = FileInputStream(fileName); new Properties props = Properties(); props.load(stream); stream.close(); return props; } [java] view plain copy
- //不正確地在一個方法中獲取、使用和釋放資源
—— 不要這樣作 - 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 機制來確保流被關閉,而不論是否發生錯誤,代碼以下所示(不過他並不完善)uiJava代碼
- //正確地在一個方法中獲取、使用和釋放資源
public static Properties loadProperties(String fileName) throws IOException { new FileInputStream stream = FileInputStream(fileName); try { new Properties props = Properties(); props.load(stream); return props; } finally { stream.close(); } } [java] view plain copy
- //正確地在一個方法中獲取、使用和釋放資源
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 塊拋出的異常取代致使塊退出的異常,這意味着原來的異常丟失了,不能用於幫助進行調試。spa
使用
finally
來釋放在方法中獲取的資源是可靠的,可是當涉及多個資源時,很容易變得難以處理。下面考慮這樣一個方法,它使用一個 JDBCConnection
來執行查詢和迭代ResultSet
。該方法得到一個Connection
,使用它來建立一個Statement
,並執行Statement
以獲得一個ResultSet
。可是中間 JDBC 對象Statement
和ResultSet
具備它們本身的close()
方法,而且當您使用完以後,應該釋放這些中間對象。然而,進行資源釋放的 「明顯的」 方式並不起做用,以下所示:.netJava代碼
- //不成功的釋放多個資源的企圖
—— 不要這樣作 - public
void enumerateFoo() throwsSQLException { null; Statement statement = null; ResultSet resultSet = Connection connection = getConnection(); try { statement = connection.createStatement(); "SELECT resultSet = statement.executeQuery( * );FROM Foo" // Use resultSet } finally { if (resultSet null)!= resultSet.close(); if (statement null)!= statement.close(); connection.close(); } } [java] view plain copy
- //不成功的釋放多個資源的企圖
—— 不要這樣作 - public
void enumerateFoo() throwsSQLException { Statement statement = null; ResultSet resultSet = null; Connection connection = getConnection(); try { statement = connection.createStatement(); resultSet = statement.executeQuery("SELECT * );FROM Foo" // Use resultSet } finally { if (resultSet null)!= resultSet.close(); if (statement null)!= statement.close(); connection.close(); } }
這個 「解決方案」 不成功的緣由在於, ResultSet
和Statement
的close()
方法本身能夠拋出SQLException
,這會致使後面 finally 塊中的close()
語句不執行。您在這裏有幾種選擇,每一種都很煩人:用一個try..catch
塊封裝每個close()
,可使用嵌套try...finally
塊,或者編寫某種小型框架用於管理資源獲取和釋放。 Java代碼
- //可靠的釋放多個資源的方法
- public
void enumerateBar() throwsSQLException { null; Statement statement = null; ResultSet resultSet = Connection connection = getConnection(); try { statement = connection.createStatement(); "SELECT resultSet = statement.executeQuery( * );FROM Bar" // Use resultSet } finally { try { if (resultSet null)!= resultSet.close(); } finally { try { if (statement null)!= statement.close(); } finally { connection.close(); } } } } [java] view plain copy
- //可靠的釋放多個資源的方法
- public
void enumerateBar() throwsSQLException { Statement statement = null; ResultSet resultSet = null; Connection connection = getConnection(); try { statement = connection.createStatement(); resultSet = statement.executeQuery("SELECT * );FROM Bar" // Use resultSet } finally { try { if (resultSet null)!= resultSet.close(); } finally { try { if (statement null)!= statement.close(); } finally { connection.close(); } } } }
咱們都知道應該使用 finally
來釋放像數據庫鏈接這樣的重量級對象,可是咱們並不老是這樣細心,可以記得使用它來關閉流(畢竟,終結器會爲咱們作這件事,是否是?)。很容易忘記在使用資源的代碼不拋出已檢查的異常時使用finally
。以下代碼展現了針對綁定鏈接的add()
方法的實現,它使用Semaphore
來實施綁定,並有效地容許客戶機等待空間可用:Java代碼
- //綁定鏈接的脆弱實現
—— 不要這樣作 - public
class LeakyBoundedSet { private final Set set = ... private final Semaphore sem; public LeakyBoundedSet( intbound) { new sem = Semaphore(bound); } public boolean add(T throwso) InterruptedException { sem.acquire(); boolean wasAdded = set.add(o); if (!wasAdded) sem.release(); return wasAdded; } - }
[java] view plain copy
- //綁定鏈接的脆弱實現
—— 不要這樣作 - public
class LeakyBoundedSet { private final Set set = ... private final Semaphore sem; public LeakyBoundedSet( intbound) { sem = new Semaphore(bound); } public boolean add(T throwso) InterruptedException { sem.acquire(); boolean wasAdded = set.add(o); if (!wasAdded) sem.release(); return wasAdded; } - }
LeakyBoundedSet
首先等待一個許可證成爲可用的(表示鏈接中有空間了),而後試圖將元素添加到鏈接中。添加操做若是因爲該元素已經在鏈接中了而失敗,那麼它會釋放許可證(由於它不實際使用它所保留的空間)。與
LeakyBoundedSet
有關的問題沒有必要立刻跳出:若是Set.add()
拋出一個異常呢?若是Set
實現中有缺陷,或者equals()
或hashCode()
實現(在SortedSet
的狀況下是compareTo()
實現)中有缺陷,緣由在於添加元素時元素已經在Set
中了。固然,解決方案是使用finally
來釋放信號量許可證,這是一個很簡單卻容易被遺忘的方法。這些類型的錯誤不多會在測試期間暴露出來,於是成了定時炸彈,隨時可能爆炸。以下代碼展現了BoundedSet
的一個更加可靠的實現:Java代碼
- //使用一個
Semaphore 來可靠地綁定 Set - public
class BoundedSet { private final Set set = ... private final Semaphore sem; public BoundedHashSet( intbound) { new sem = Semaphore(bound); } public boolean add(T throwso) InterruptedException { sem.acquire(); boolean wasAdded false;= try { wasAdded = set.add(o); return wasAdded; } finally { if (!wasAdded) sem.release(); } } - }
[java] view plain copy
- //使用一個
Semaphore 來可靠地綁定 Set - public
class BoundedSet { private final Set set = ... private final Semaphore sem; public BoundedHashSet( intbound) { sem = new Semaphore(bound); } public boolean add(T throwso) InterruptedException { sem.acquire(); boolean wasAdded false;= try { wasAdded = set.add(o); return wasAdded; } finally { if (!wasAdded) sem.release(); } } - }
對於具備任意生命週期的資源,咱們要回到 C 語言的時代,即手動地管理資源生命週期。在一個服務器應用程序中,客戶機到服務器的一個持久網絡鏈接存在於一個會話期間(好比一個多人蔘與的遊戲服務 器),每一個用戶的資源(包括套接字鏈接)在用戶退出時必須被釋放。好的組織是有幫助的;若是對每一個用戶資源的角色引用保存在一個 ActiveUser 對象中,那麼它們就能夠在 ActiveUser 被釋放時(不管是顯式地釋放,仍是經過垃圾收集而釋放)而被釋放。
具備任意生命週期的資源幾乎老是存儲在一個全局集合中(或者從這裏可達)。要避免資源泄漏,所以很是重要的是,要識別出資源什麼時候再也不須要了並能夠從這個全局集合中刪除了。此時,由於您知道資源將要被釋放,任何與該資源關聯的非內存資源也能夠同時被釋放。
確保及時的資源釋放的一個關鍵技巧是維護全部權的一個嚴格層次結構,其中的全部權具備釋放資源的職責。若是應用程序建立一個線程池,而線程池建立線 程,線程是程序能夠退出以前必須被釋放的資源。可是應用程序不擁有線程,而是由線程池擁有線程,所以線程池必須負責釋放線程。固然,直到它自己被應用程序 釋放以後,線程池才能釋放線程。
維護一個全部權層次結構有助於不至於失去控制,其中每一個資源擁有它得到的資源並負責釋放它們。這個規則的結果是,每一個不能由垃圾收集單獨收集的資源(即這樣的資源,它直接或間接擁有不能由垃圾收集釋放的資源)必須提供某種生命週期支持,好比close() 方法。
若是說平臺庫提供終結器來清除打開的文件句柄,這大大下降了忘記顯式地關閉這些句柄的風險,爲何不更多地使用終結器呢?緣由有不少,最重要的一個 緣由是,終結器很難正確編寫(而且很容易編寫錯)。終結器不只難以編寫正確,終結的定時也是不肯定的,而且不能保證終結器最終會運行。而且終結還爲可終結 對象的實例化和垃圾收集帶來了開銷。不要依賴於終結器做爲釋放資源的主要方式。
垃圾收集爲咱們作了大量可怕的資源清除工做,可是有些資源仍然須要顯式的釋放,好比文件句柄、套接字句柄、線程、數據庫鏈接和信號量許可證。當資源的生命週期被綁定到特定調用幀的生命週期時,咱們一般可使用
finally
塊來釋放該資源,可是長期存活的資源須要一種策略來確保它們最終被釋放。對於任何一個這樣的對象,即它直接或間接擁有一個須要顯式釋放的對象,您必須提供生命週期方法 —— 好比close()
、release()
、destroy()
等 —— 來確保可靠的清除。