我们的父母过去常常提醒我们,当我们完成玩具后,就把它们收起来。 如果您仔细观察,那么motivation的动机可能不是使事物保持清洁的抽象愿望,而是实际的限制,即房屋中只有这么多的地板空间,如果覆盖着玩具,它就会不能用于其他事情-例如走动。
如果有足够的空间,就会减少清理人头的动力。 您拥有的空间越多,保持清洁的动力就越少。 阿罗·格斯里(Arlo Guthrie)著名的民谣爱丽丝大屠杀(Restaurant Massacre)说明了这一点:
拥有所有房间,看到它们如何清除所有的座位,他们决定不必长时间清理垃圾。
不管是好是坏,垃圾收集会使我们对自己清理后有些草率。
明确释放资源
Java程序中使用的绝大部分资源都是对象,垃圾回收可以很好地清理它们。 继续,根据需要使用任意多个String
。 垃圾收集器最终会在没有您帮助的情况下弄清楚它们何时不再有用,并回收他们使用的内存。
另一方面,程序必须使用诸如close()
, destroy()
, shutdown()
或release()
类的名称的方法显式释放文件句柄和套接字句柄之类的非内存资源。 某些类(例如平台类库中的文件处理流实现)将终结器提供为“安全网”,因此,如果程序忘记释放资源,则当垃圾回收器确定该程序时,终结器仍可以完成此工作。完成了。 但是,即使您忘记了文件句柄,也可以在您完成之后清除终结器,但在完成处理后,最好显式关闭它们。 这样做可以比以前更早地关闭它们,从而减少了资源耗尽的机会。
对于某些资源,无法等待最终确定以释放它们。 对于诸如锁获取和信号量许可之类的虚拟资源, 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.释放多个资源的尝试失败—不要这样做
public void enumerateFoo() throws SQLException {
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
块嵌套,如清单4所示,或者编写某种微型框架来进行管理资源获取和释放。
清单4.可靠(如果笨拙)的方式释放多个资源
public void enumerateBar() throws SQLException {
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();
}
}
}
}
private Connection getConnection() {
return null;
}
几乎所有东西都会抛出异常
大家都知道我们应该finally
使用它来释放数据库连接之类的重量级对象,但是对于使用它来关闭流,我们并不总是那么小心(毕竟,终结器会为我们做到这一点,对吗?)。 当使用资源的代码未引发检查异常时,也很容易忘记finally
使用。 清单5显示了一个有界集合的add()
方法的实现,该集合使用Semaphore
来强制执行该界限并有效地允许客户端等待空间变得可用:
清单5.有限集合的脆弱实现—不要这样做
public class LeakyBoundedSet<T> {
private final Set<T> set = ...
private final Semaphore sem;
public LeakyBoundedSet(int bound) {
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded = set.add(o);
if (!wasAdded)
sem.release();
return wasAdded;
}
}
LeakyBoundedSet
首先等待许可可用(指示集合中有空间),然后尝试将元素添加到集合中。 如果添加操作由于元素已经在集合中而失败,则它将释放许可(因为它实际上并未使用其保留的空间)。
LeakyBoundedSet
的问题不一定立即跳出来:如果Set.add()
引发异常怎么办? 之所以可能发生这种情况,是因为要添加元素或元素的Set
实现存在缺陷,或者equals()
或hashCode()
实现(或者在SortedSet
的情况下是compareTo()
实现equals()
存在缺陷。已在Set
。 解决的方法当然是finally
使用释放信号灯许可证。 一种足够容易但却经常被人们遗忘的方法。 这些类型的错误在测试过程中很少被发现,这使它们成为定时炸弹,等待熄灭。 清单6显示了BoundedSet
一个更可靠的BoundedSet
:
清单6.使用信号量可靠地绑定Set
public class BoundedSet<T> {
private final Set<T> set = ...
private final Semaphore sem;
public BoundedHashSet(int bound) {
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
}
finally {
if (!wasAdded)
sem.release();
}
}
}
代码审核工具如FindBugs的(见相关信息 )可以检测不正确的资源释放的一些实例,比如在一个方法打开一个流,而不是将其关闭。
具有任意生命周期的资源
对于具有任意生命周期的资源,我们回到了使用C的状态-手动管理资源生命周期。 在客户端在会话期间与服务器建立持久网络连接的服务器应用程序中(例如多人游戏服务器),当用户登录时,必须释放每个用户获取的任何资源(包括套接字连接)出来。 良好的组织可以提供帮助; 如果对每个用户资源的唯一引用保存在ActiveUser对象中,则可以在释放ActiveUser时释放它们(无论是显式还是通过垃圾回收)。
几乎可以肯定,具有任意生命周期的资源将存储在某个地方的全局集合中(或从中访问)。 为了避免资源泄漏,因此至关重要的是确定何时不再需要该资源并将其从此全局集合中删除。 (以前的文章“ 使用弱引用堵塞内存泄漏 ”提供了一些有用的技术。)此时,由于您知道资源即将被释放,因此与该资源关联的任何非内存资源也可以在此时释放。
资源所有权
确保及时释放资源的关键技术是保持所有权的严格等级; 拥有所有权带来了释放资源的责任。 如果应用程序创建了线程池,并且线程池创建了线程,则线程是必须在程序退出之前释放(允许终止)的资源。 但是应用程序不拥有线程; 线程池必须这样做,因此线程池必须负责释放它们。 当然,在应用程序释放线程池本身之前,它无法释放它们。
维护所有权层次结构,其中每个资源都拥有它所获取的资源并负责释放它们,这有助于避免混乱情况一发不可收拾。 此规则的结果是,不能完全由垃圾收集释放的每个资源(包括直接或间接拥有不能仅由垃圾收集释放的资源的任何资源)必须提供某种生命周期支持,例如close()
方法。
终结者
如果平台库提供了终结器来清理打开的文件处理程序,从而大大降低了忘记显式关闭它们的风险,那么为什么不更频繁地使用终结器呢? 原因有很多,最重要的是,终结器正确地编写非常棘手(并且很容易错误地编写)。 不仅很难正确地编码它们,而且终结的时间不确定,而且不能保证终结器甚至可以运行。 终结处理增加了可终结对象的实例化和垃圾回收的开销。 不要依赖终结器作为释放资源的主要手段。
摘要
垃圾回收对我们进行了很多清理工作,但是某些资源仍然需要显式释放,例如文件句柄,套接字句柄,线程,数据库连接和信号量许可。 如果资源的生命周期与特定调用框架的生命周期息息相关,那么我们通常可以不使用finally
块来释放资源,但是寿命更长的资源需要一种确保最终释放资源的策略。 对于可能直接或间接拥有需要显式释放的对象的任何对象,必须提供生命周期方法( close()
, release()
, destroy()
等)以确保可靠的清除。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp03216/index.html