当你从手工管理内存的语言(好比C或者C++)转换到具备垃圾回收功能的语言的时候,程序猿的工做就会变得更加容易,由于当你用完了对象以后,他们就会被自动回收。当你第一次经历对象回收功能的时候,会以为这简直有点难以想象。这很容易给你留下这样的印象,认为本身再也不须要考虑内存管理的事情了,其实否则。java
考虑下面这个简单的栈实现的例子:程序员
// 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 ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
这个程序没有明显的错误(它的通用版本请见29项)。不管如何测试,它都会成功地经过每一项测试,可是这个程序中隐藏着一个问题。简而言之,改程序存在“内存泄漏”,因为垃圾收集器的活动增长或者内存占用增长,程序性能的下降会逐渐表现出来。在极端的状况下,这种内存泄漏会致使磁盘分页(Disk Paging),甚至致使程序失败并出现OutOfMemoryError,但这种失败情形相对比较少见。编程
那么,程序中哪里发生了内存泄漏呢?若是一个栈先是增加,而后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即便使用栈的程序再也不引用这些对象,它们也不会回收,由于,栈内部维护着对这些对象的过时引用(obsolete references),所谓的过时引用,是指永远也不会再被解除的引用。在本例中,凡是在element数组的“活动部分”(active portion)以外的任何引用都是过时的。活动部分是指element中下标小于size的那些元素。数组
具备垃圾收集功能的编程语言中的内存泄漏(更恰当地称为无心识的对象保留)是隐蔽的。若是无心中保留了对象引用,则不只将该对象从垃圾回收中排除,并且该对象引用的任何对象也是如此,依此类推。即便无心中保留了少许对象引用,也会阻止许多对象被垃圾回收器收集,对性能可能产生很大影响。缓存
这类问题的修复方法很简单:一旦对象引用已通过期,只须要清空这些引用便可。对于上述例子中的Stack类而言,只要一个单元被弹出栈,指向它的引用就过时了,pop方法的修订版本以下所示:编程语言
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; }
清空过时引用的另外一个好处是,若是它们之后又被错误地解除引用,程序就会当即抛出NullPointerException异常,而不是悄悄地错误运行下去。尽快检测出程序中的错误老是有益的。工具
当程序员第一次被相似这样的问题困扰的时候,它们每每会过度当心:对于每个对象的引用,一旦程序再也不用到它,就把它清空。其实这样作即不必,也不是咱们所指望的,由于这样作会把程序代码弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过时引用最好的方法是让包含该引用的变量结束其生命周期。若是你是在最紧凑的做用域范围内定义每个变量(第57项),这种情形就会天然而然地发生。性能
那么,什么时候应该清空引用呢?Stack类的哪方面特性使它易于遭受内存泄漏的影响呢?简而言之,问题在于,Stack类本身管理内存(manage its own memory)、存储池(storage pool)包含了elements数组(对象引用单元,而不是对象自己)的元素。数组活动区域(同前面的定义)中的元素是已分配的(allocated),而数组其他部分的元素则是自由的(free)。可是垃圾回收器没法知道这一点;对于垃圾回收器而言,elements数组中的全部对象引用都同等有效。只有程序猿知道数组的非活动部分是不重要的。程序猿能够把这个状况告知垃圾回收器,作法很简单:一旦数组元素变成了非活动部分的一部分,程序猿就手动清空这些数组元素。测试
一般来讲,只要类是本身管理内存,程序猿就应该警戒内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。spa
内存泄漏的另外一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它在很长一段时间没有使用,可是却仍然留在缓存中。对于这个问题,这里有好几种解决方案。若是你正好要实现这样的缓存,只要在缓存以外存在对某个项的键的引用,该项就有意义,那么就能够用WeakHashMap表明缓存,当缓存中的项过时以后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
更为常见的情形则是,“缓存项的生命周期是否有意义”并非很容易肯定,随着时间的推移,其中的项会变得愈来愈没有价值。在这种状况下,缓存应该时不时地清除掉没用的项。这项清除工做能够由一个后台线程(多是Timer或者ScheduledThreadPoolExecutor)来完成,或者也能够在给缓存添加新项的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法能够很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref。
内存泄漏的第三个常见来源是监听器和其余回调。若是你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,那么除非你采起某些动做,不然他们就会积累下来。确保回调当即被当作垃圾回收的最佳方法是只保存他们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。
因为内存泄漏一般不会表现出明显的失败迹象,因此他们能够在一个系统中存在不少年。每每只有经过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。所以,若是能在内存泄漏发生以前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。