7. 消除过期的对象引用

      考虑以下简单的栈实现:

      这个程序没有什么明显的错误(但是对于泛型版本,请参阅条目 29)。 你可以对它进行详尽的测试,它都会成功地通过每一项测试,但有一个潜在的问题。 笼统地说,程序有一个「内存泄漏」,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页(disk paging),甚至导致内存溢出(OutOfMemoryError)的失败,但是这样的故障相对较少。

      那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用(obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组「活动部分(active portion)」之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。

      这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的 类的情景下,只要从栈中弹出,元素的引用就设置为过期。 pop 方法的修正版本如下所示:

    1. public Object pop() {
    2. throw new EmptyStackException();
    3. }

      取消过期引用的另一个好处是,如果它们随后被错误地引用,程序立即抛出 NullPointerException 异常,而不是悄悄地做继续做错误的事情。尽可能快地发现程序中的错误是有好处的。

      当程序员第一次被这个问题困扰时,他们可能会在程序结束后立即清空所有对象引用。这既不是必要的,也不是可取的;它不必要地搞乱了程序。清空对象引用应该是例外而不是规范。消除过期引用的最好方法是让包含引用的变量超出范围。如果在最近的作用域范围内定义每个变量 (详见第 57 条),这种自然就会出现这种情况。

      一般来说,当一个类自己管理内存时,程序员应该警惕内存泄漏问题。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。

      另一个常见的内存泄漏来源是缓存。 一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。对于这个问题有几种解决方案。如果你正好想实现了一个缓存:只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用 WeakHashMap 来表示缓存;这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap 才有用。

      更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程 (也许是 ScheduledThreadPoolExecutor) 或将新的项添加到缓存时顺便清理。LinkedHashMap 类使用它的 removeEldestEntry 方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用 。

      因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器(heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。