Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。java
若是你从使用手动内存管理的语言(如C或c++)切换到像Java这样的带有垃圾收集机制的语言,那么做为程序员的工做就会变得容易多了,由于你的对象在使用完毕之后就自动回收了。当你第一次体验它的时候,它就像魔法同样。这很容易让人以为你不须要考虑内存管理,但这并不彻底正确。c++
考虑如下简单的堆栈实现:程序员
// 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)。 过时引用简单来讲就是永远不会解除的引用。 在这种状况下,元素数组“活动部分(active portion)”以外的任何引用都是过时的。 活动部分是由索引下标小于size的元素组成。缓存
垃圾收集语言中的内存泄漏(更适当地称为无心的对象保留 unintentional object retentions)是隐蔽的。 若是无心中保留了对象引用,那么不只这个对象排除在垃圾回收以外,并且该对象引用的任何对象也是如此。 即便只有少数对象引用被无心地保留下来,也能够阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。工具
这类问题的解决方法很简单:一旦对象引用过时,将它们设置为 null。 在咱们的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
类的哪一个方面使它容易受到内存泄漏的影响?简单地说,它管理本身的内存。存储池(storage pool)由elements
数组的元素组成(对象引用单元,而不是对象自己)。数组中活动部分的元素(如前面定义的)被分配,其他的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来讲,elements
数组中的全部对象引用都一样有效。只有程序员知道数组的非活动部分不重要。程序员能够向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就能够手动清空这些元素的引用。spa
通常来讲,当一个类本身管理内存时,程序员应该警戒内存泄漏问题。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。
另外一个常见的内存泄漏来源是缓存。一旦将对象引用放入缓存中,很容易忘记它的存在,而且在它变得可有可无以后,仍然保留在缓存中。对于这个问题有几种解决方案。若是你正好想实现了一个缓存:只要在缓存以外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就能够用WeakHashMap
来表示缓存;这些项在过时以后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap
才有用。
更常见的状况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得愈来愈没有价值。在这种状况下,缓存应该偶尔清理掉已经废弃的项。这能够经过一个后台线程(也许是ScheduledThreadPoolExecutor
)或将新的项添加到缓存时顺便清理。LinkedHashMap
类使用它的removeEldestEntry
方法实现了后一种方案。对于更复杂的缓存,可能直接须要使用java.lang.ref
。
第三个常见的内存泄漏来源是监听器和其余回调。若是你实现了一个API,其客户端注册回调,可是没有显式地撤销注册回调,除非采起一些操做,不然它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在WeakHashMap
的键(key)中。
由于内存泄漏一般不会表现为明显的故障,因此它们可能会在系统中保持多年。 一般仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现。 所以,学习如何预见这些问题,并防止这些问题发生,是很是值得的。