如果你从手动管理内存的语言比如C或者C++,转到垃圾收集语言比如java,那么基于垃圾自动回收机制,在用完一些对象以后,你作为开发人员的工作就会变的小了很多。如果你第一次经历这些,你会感觉好像magic一样。它很容易给你留下你不用去考虑内存管理的这种印象,但是这是不对的。考虑下面这个简单的栈的实现:
// Can you spot the "memory leak"?
public class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public Stack() {
elements = newObject[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 moreelement, roughly
* doubling the capacity each time the arrayneeds to grow.
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, (2 * size) + 1);
}
}
}
这个程序没有什么明显的错误(除了参考Item 29那个泛型的版本)。可能费尽力气去测也会发现,它会通过每一次测试,但是着实是有一个潜在的问题的。笼统的说,它有一个“内存泄漏”问题,随着垃圾回收活动的增加或者内存足迹的增多,就会导致性能的下降,与此同时这个问题就会悄悄地慢慢体现出来了。在极端的情况下,这种内存泄漏会引起磁盘分页,甚至会引起OutOfMemoryError的程序错误,但是这种情况相对会非常少。
那么内存泄漏在哪里呢?如果栈存储的对象进去的多了,然后又出来了很多,那么从栈里弹出的那些对象将永远不会被垃圾回收到,尽管程序所使用的栈已经没有那些弹出对象的引用。原因是栈保留着这些对象的废弃的引用。废弃的引用可以简单认为是一种引用,不过这种引用永远都不能再被关联起来。在这种情况下,一个数组中除了“存活部分”以外的任何引用都是废弃的。而存活部分的意思是它里面的元素的索引都是小于集合的容量(size)的。
垃圾回收语言中的内存泄漏问题(称为非计划的对象保留更合适些)是隐晦的。如果一个对象引用非计划的保留了,不仅仅这个对象会被垃圾回收排除掉,而且还有这个对象所用到的任何对象也会这样。因此尽管只有一些对象引用被非计划保留了,但是很多很多对象就不能被垃圾回收,这会对性能带来很大潜在的影响。
修改这种问题很简单:一旦引用变成废弃的,就把引用设置成null。在我们的栈的例子里面,只要一个对象弹出这个栈,栈对这个对象的引用就变成了过时的。Pop方法的正确版本长这个样子:
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null; // Eliminateobsolete reference
return result;
}
将废弃引用写成null的附加的好处是,如果它们随后又被不小心简介引用了,程序立马会抛出一个NullPointerException然后挂掉,而不是悄悄地做错误的事。尽快的找到程序的错误通常都是有好处的。当这个问题第一次刺痛开发人员时,他们可能在程序一完成使用栈就粗暴的将所有对象引用都赋值为null。这种做法既不必要也不应该;它会给程序带来不必要的杂乱。将对象设置为null应该在异常的情况下而不是正常情况。消除废弃引用的做好方式是让包含那种引用的对象脱离它所在的范围。如果你把每个参数的范围都能定义为可能的最小范围,上面所说的情况对它而言就是自然而然的事情(Item 57)。
那么什么时候你应该把引用设置为null呢?上面那个stack类的那个点使得内存泄漏变得可能?简单而言,它自己管理自己的内存。存储池由元素数组(对象引用单元,并不是对象本身)中的元素组成。那些数组(Object[] elements上面所定义的)中的存活部分的元素要去分配,数组中剩下的部分要去释放。但是,垃圾收集器是没有办法知道这一点的;对于垃圾收集器而言,元素数组中的所有对象引用一样都是有效的。只有开发人员会知道数组中没有存活的部分是不重要的。所以,一旦数组对象变成了非存活的那部分,开发人员就要通过手动地将数组元素设置为null来及时将这一事实告知垃圾收集器。
总的来说,无论什么时候一个类自己管理内存,开发人员都要去小心内存泄漏。无论什么时候一个元素被释放了,那么这个元素所包含的任何对象引用都应该被设置为null。另一种常见的内存泄漏的原由是缓存。一旦一个对象引用被放在了缓存里面,在它变得无用的时候很容易会忘记它还在那里,并且它会一直留在那里。这种问题有几种解决方案。很幸运,如果你去实现一种缓存,对这个缓存而言,只要在缓存的外部有它的key的引用,那么这个entry就是有用的,那么可以用WeakHashMap来代表它;它里面的entries一旦废弃,他们就会被移除掉。需要记住的是,缓存中entries的预设存活时间仅仅被外部的key而不是value的引用决定的时候,WeakHashMap才是有用的。
更为常见的是,随着时间的推移,缓存中的entries变得没那么有意义,这种缓存中entry的有效生命期就被定义没那么好。在这种情况下,那些已经变得不会去使用的entries应该时不时的清理一下。这种可以用背后的线程(也许一个ScheduledThreadPoolExecutor)去做或者当做给缓存中添加entries的副作用。
LinkedHashMap类的removeEldestEntry方法可以帮助实现后者。在复杂的情况,你可能就要直接使用java.lang.ref。
第三种内存泄漏常见的原因是监听者或者其他的回调。如果你实现了某个API,API的客户端要注册回调,但是没有显式的注销它们,如果你不做一些事情,它们就会增多。一种确保回调能被及时的垃圾回收的方式是,只去存储它们的弱引用,比如,把他们以Keys的方式存在WeakHashMap。因为内存泄漏通常不会以明显的错误来表现出它们自身,所以他们可能在若干年之后仍会变现出来。它们只能通过仔细的检查代码或者使用heap profiler的调试工具才能找出来。因此,学习在这种在问题发生之前就去找到问题,阻止它们发生是非常必要的。