《Effective Java》笔记07:消除过期的对象引用

  1. 修改方式
  2. 不要被类似的问题困扰
  3. 何时清空引用
  4. 内存泄漏常见来源
    1. 缓存
    2. 缓存项的生命周期是否意义
    3. 监听器和其他回调

如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收。即使使用栈的程序不再引用这些对象,它们也不会被回收。

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];
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copys(elements, 2 * size + 1);
    }
}

以上示例代码,在正常的使用中不会产生任何逻辑问题,然而随着程序运行时间不断加长,内存泄露造成的副作用将会慢慢的显现出来,如磁盘页交换、OutOfMemoryError 等。因为栈内部维护着对这些对象的过期引用,永远不会被解除。

修改方式

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 手工将数组中的该对象置空
    return result;
}

清空过期引用的另一个好处是,如果他们以后又被错误的解除引用,程序就会立刻抛出 NullPointerException 异常,而不是悄悄的错误运行下去。

不要被类似的问题困扰

当程序员第一次被类似这样的问题困扰的时候,他们往往会过分小心;对于每一个对象引用,一旦不再使用它,就把它清空,这是没有必要的,这样反而会把代码弄的混乱。清空对象的引用应该是一种例外,而不是一种规范行为。消除过期引用最好的办法是让包含该引用的变量结束其生命周期。如果是在最紧凑的作用域范围内定义每一个变量,这种情况就会自然而然地发生。

何时清空引用

一旦数组元素变成了非活动部分的一部分,就手工清空这些数组元素。

一般而言,只要类是自己管理内存,就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

内存泄漏常见来源

缓存

一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap(弱键映射,允许垃圾回收器回收无外界引用指向象 Map 中键)代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的任命周期是由该键的外部引用而不是由值决定时,WeakHashMap 才有用外。

缓存项的生命周期是否意义

并不是很容易确定,随着时间的推移,其中的项会变的越来越没有价值,这种情况下,缓存应该时不时的清楚掉没有用的项。这项清楚工作可以由一个后台线程(可能是 Timer 或者 ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap 类中 removeEldestEntry 方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用 java.lang.ref

如果要实现前一种功能,我们需继承 LinkedHashMap 并重写它的 removeEldestEntry 方法(默认返回false,即不会删除最旧项),putputAll 将调用此方法,下面是测试项

public class CacheLinkedHashMap extends LinkedHashMap {
    // 允许最大放入的个数,超过则可能删除最旧项
    private static final int MAX_ENTRIES = 5;

    @Override
    // 是否删除最旧项(最先放入)实现
    protected boolean removeEldestEntry(Map.Entry eldest) {
        Integer num = (Integer) eldest.getValue();  // 最早项的值
        // 如果老的项小于3且已达到最大允许容量则会删除最老的项
        if (num.intValue() < 3 && size() > MAX_ENTRIES) {
            System.out.println("超容 - " + this);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        CacheLinkedHashMap lh = new CacheLinkedHashMap();
        for (int i = 1; i <= 5; i++) {
            lh.put("K_" + Integer.valueOf(i), Integer.valueOf(i));
        }
        System.out.println(lh);
        // 放入时会删除最早放入的 k_1 项
        lh.put("K_" + Integer.valueOf(11), Integer.valueOf(0));
        System.out.println(lh);
    }
}

输出:

{K_1=1, K_2=2, K_3=3, K_4=4, K_5=5}
超容 - {K_1=1, K_2=2, K_3=3, K_4=4, K_5=5, K_11=0}
{K_2=2, K_3=3, K_4=4, K_5=5, K_11=0}

监听器和其他回调

如果你实现了一个 API,客户端在这个 API 中注册回调(即将回调实例存储到某个容器中),却没有显示地取消注册,那么除非你采取某些动作,否则它们就会积聚。确保立即被垃圾回收的最佳方法是只保存它的弱引用,例如,只将它们保存成 WeakHashMap 中的键。

内存泄漏剖析工具:Heap Profiler


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Effective Java》笔记07:消除过期的对象引用

文章字数:1.3k

本文作者:Bin

发布时间:2016-11-04, 15:28:51

最后更新:2019-08-06, 00:42:47

原始链接:http://coolview.github.io/2016/11/04/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B007%EF%BC%9A%E6%B6%88%E9%99%A4%E8%BF%87%E6%9C%9F%E7%9A%84%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录