effective-java-3rd-chinese/07. 消除过期的对象引用.md

76 lines
6.4 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 7. 消除过期的对象引用
  如果你从使用手动内存管理的语言 (如 C 或 C++) 切换到像 Java 这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次体验它的时候,它就像魔法一样。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。
  考虑以下简单的堆栈实现:
```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。 过期引用简单来说就是永远不会解除的引用。 在这种情况下元素数组“活动部分active portion”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。
  垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions是隐蔽的。 如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。
  这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的 `Stack` 类的情景下,只要从栈中弹出,元素的引用就设置为过期。 `pop` 方法的修正版本如下所示:
```Java
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` 数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。
  一般来说,**当一个类自己管理内存时,程序员应该警惕内存泄漏问题。** 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。
  **另一个常见的内存泄漏来源是缓存。** 一旦将对象引用放入缓存中很容易忘记它的存在并且在它变得无关紧要之后仍然保留在缓存中。对于这个问题有几种解决方案。如果你正好想实现了一个缓存只要在缓存之外存在对某个项entry的键key引用那么这项就是明确有关联的就可以用 `WeakHashMap` 来表示缓存这些项在过期之后自动删除。记住只有当缓存中某个项的生命周期是由外部引用到键key而不是值value决定时`WeakHashMap` 才有用。
  更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程 (也许是 `ScheduledThreadPoolExecutor`) 或将新的项添加到缓存时顺便清理。`LinkedHashMap` 类使用它的 `removeEldestEntry` 方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用 `java.lang.ref`
  第三个常见的内存泄漏来源是监听器和其他回调。如果你实现了一个 API其客户端注册回调但是没有显式地撤销注册回调除非采取一些操作否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用weak references例如仅将它们保存在 `WeakHashMap` 的键key中。
  因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。