摘要
虽然Java虚拟机(JVM)及其垃圾收集器(garbage collector,GC)负责治理大多数的内存任务,Java软件程序中还是有可能出现内存泄漏。实际上,这在大型项目中是一个常见的问题。避免内存泄漏的第一步是要弄清楚它是如何发生的。本文介绍了编写Java代码的一些常见的内存泄漏陷阱,以及编写不泄漏代码的一些最佳实践。一旦发生了内存泄漏,要指出造成泄漏的代码是非常困难的。因此本文还介绍了一种新工具,用来诊断泄漏并指出根本原因。该工具的开销非常小,因此可以使用它来寻找处于生产中的系统的内存泄漏。
垃圾收集器的作用
虽然垃圾收集器处理了大多数内存治理问题,从而使编程人员的生活变得更轻松了,但是编程人员还是可能犯错而导致出现内存问题。简单地说,GC循环地跟踪所有来自“根”对象(堆栈对象、静态对象、JNI句柄指向的对象,诸如此类)的引用,并将所有它所能到达的对象标记为活动的。程序只可以操纵这些对象;其他的对象都被删除了。因为GC使程序不可能到达已被删除的对象,这么做就是安全的。
虽然内存治理可以说是自动化的,但是这并不能使编程人员免受思考内存治理问题之苦。例如,分配(以及释放)内存总会有开销,虽然这种开销对编程人员来说是不可见的。创建了太多对象的程序将会比完成同样的功能而创建的对象却比较少的程序更慢一些(在其他条件相同的情况下)。
而且,与本文更为密切相关的是,假如忘记“释放”先前分配的内存,就可能造成内存泄漏。假如程序保留对永远不再使用的对象的引用,这些对象将会占用并耗尽内存,这是因为自动化的垃圾收集器无法证实这些对象将不再使用。正如我们先前所说的,假如存在一个对对象的引用,对象就被定义为活动的,因此不能删除。为了确保能回收对象占用的内存,编程人员必须确保该对象不能到达。这通常是通过将对象字段设置为null或者从集合(collection)中移除对象而完成的。但是,注重,当局部变量不再使用时,没有必要将其显式地设置为null。对这些变量的引用将随着方法的退出而自动清除。
概括地说,这就是内存托管语言中的内存泄漏产生的主要原因:保留下来却永远不再使用的对象引用。
典型泄漏
既然我们知道了在Java中确实有可能发生内存泄漏,就让我们来看一些典型的内存泄漏及其原因。
全局集合
在大的应用程序中有某种全局的数据储存库是很常见的,例如一个JNDI树或一个会话表。在这些情况下,必须注重治理储存库的大小。必须有某种机制从储存库中移除不再需要的数据。
这可能有多种方法,但是最常见的一种是周期性运行的某种清除任务。该任务将验证储存库中的数据,并移除任何不再需要的数据。
另一种治理储存库的方法是使用反向链接(referrer)计数。然后集合负责统计集合中每个入口的反向链接的数目。这要求反向链接告诉集合何时会退出入口。当反向链接数目为零时,该元素就可以从集合中移除了。
缓存
缓存是一种数据结构,用于快速查找已经执行的操作的结果。因此,假如一个操作执行起来很慢,对于常用的输入数据,就可以将操作的结果缓存,并在下次调用该操作时使用缓存的数据。
缓存通常都是以动态方式实现的,其中新的结果是在执行时添加到缓存中的。典型的算法是:
检查结果是否在缓存中,假如在,就返回结果。
假如结果不在缓存中,就进行计算。
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用。
该算法的问题(或者说是潜在的内存泄漏)出在最后一步。假如调用该操作时有相当多的不同输入,就将有相当多的结果存储在缓存中。很明显这不是正确的方法。
为了预防这种具有潜在破坏性的设计,程序必须确保对于缓存所使用的内存容量有一个上限。因此,更好的算法是:
检查结果是否在缓存中,假如在,就返回结果。
假如结果不在缓存中,就进行计算。
假如缓存所占的空间过大,就移除缓存最久的结果。
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用。
通过始终移除缓存最久的结果,我们实际上进行了这样的假设:在将来,比起缓存最久的数据,最近输入的数据更有可能用到。这通常是一个不错的假设。
新算法将确保缓存的容量处于预定义的内存范围之内。确切的范围可能很难计算,因为缓存中的对象在不断变化,而且它们的引用包罗万象。为缓存设置正确的大小是一项非常复杂的任务,需要将所使用的内存容量与检索数据的速度加以平衡。
解决这个问题的另一种方法是使用java.lang.ref.SoftReference类跟踪缓存中的对象。这种方法保证这些引用能够被移除,假如虚拟机的内存用尽而需要更多堆的话。
ClassLoader
Java ClassLoader结构的使用为内存泄漏提供了许多可乘之机。正是该结构本身的复杂性使ClassLoader在内存泄漏方面存在如此多的问题。ClassLoader的非凡之处在于它不仅涉及“常规”的对象引用,还涉及元对象引用,比如:字段、方法和类。这意味着只要有对字段、方法、类或ClassLoader的对象的引用,ClassLoader就会驻留在JVM中。因为ClassLoader本身可以关联许多类及其静态字段,所以就有许多内存被泄漏了。