以下是简单的栈实现的例子:
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size ++ ] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[-- size];
}
/*
* Ensure space for at least one more element,
* roughly doubling the capacity each time the array needs to grow.
*/
private void ensureCappacity() {
if(elements.length == size)
elements = Arrays.copy(elements, 2 * size + 1);
}
}
这段程序中并没有很明显的错误。无论如何测试,它都会成功的通过每一项测试,但是这个程序中隐藏着一个问题。不严格地讲,这段程序有一个“内存泄漏”,随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄露会导致磁盘交换(Disk Paging),甚至导致程序失败(OutOfMemoryError错误),但是这种失败情形相对比较少见。
那么,程序中哪里发生了内存泄漏?如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,他们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用(obsolete referrence)。所谓过期引用,是指永远也不会再被解除的引用。在本例中,凡是在elements数组的“活动部分(active portion)”之外的任何引用都是过期的。活动部分是指elements中下标小于size的哪些部分。
在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄漏为“无意识的对象保持(unintentional object retention)”更为恰当)。如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。即使只有少量的几个对象引用被无意识的保留下来,他会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。
这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。
对于上述例子中的Stack类而言,只要一个单元被弹出栈,指向他的引用过期了。pop方法的修订版本如下所示:
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[-- size];
elements[size] = null; // Elements obsolete reference
return elements[-- size];
}
清空过期引用的另一个好处是,如果他们以后又被错误的解除引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去,尽快的检测出程序中的错误总是有意义的。
清空对象引用应该是一种例外,而不是一种规范行为。因为过期引用最好的方法使让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,这种情形就会自然而然的发生。
那么,何时应该清空引用?Stack类的哪方面特性使他易于遭受内存泄露的影响呢?简而言之,问题在于,Stack类自己管理内存(manage its own memory)。存储池(storage pool)包含了elements数组(对象引用单元,而不是对象本身)的元素。数组活动区域(同前面的定义)中的元素是已分配的(allocated),而数组其余部分的元素则是自由的(free)。但是垃圾回收器并不知道这一点,对于垃圾回收器而言,elements数组中的所有对象引用都同等有效。只有程序员知道数组的非活动部分是不重要的。程序员可以把这个情况告知垃圾回收器,做法很简单:一旦数组元素变成了非活动的一部分,程序员就手工清空这些数组元素。
一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
内存泄漏的另一个常见来源是缓存。只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,他们就会自动被删除。记住只有当所有缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
内存泄漏的第三个常见来源是监听器和其他回调。确保回调立即被当做垃圾回收的最佳方法使只保存他们的弱引用(weak reference),例如,只将它们保存成WeakHashMap的键。