Effective Java 第三版读书笔记——条款7:清除过期的对象引用

Java 带有垃圾回收(garbage-collected)机制,这使程序员的工作变得容易了很多——因为你的对象在使用完毕以后就自动回收了。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。

考虑下面这个简单的栈实现:

// 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);
    }
}

这个程序没有什么明显的错误,但有一个潜在的问题——“内存泄漏”。由于垃圾回收器的活动的增加,或内存占用的增加,程序的性能会下降。

那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被当作垃圾回收掉,即使使用栈的程序不再引用这些对象。 这是因为栈维护了对这些对象的过期引用( 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;
}

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

下面是几个常见的内存泄露来源:

  • 当一个类自己管理内存时。如上面的 Stack 类,数组中活动部分的元素被分配,其余的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来说,elements 数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实:一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。

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

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

  • 监听器和其他回调。如果你实现了一个API——其客户端注册回调(callbacks),但是没有显式地撤销他们的注册。除非采取一些操作来处理,否则这些回调会积累。确保回调被垃圾回收的一种方法是只存储弱引用(weak references),例如,仅将它们保存在 WeakHashMap 的键(key)中。

posted @ 2018-11-15 10:59  LeeFire  阅读(202)  评论(0编辑  收藏  举报