Effective Java 7.消除过期的对象引用

110 阅读6分钟

当你从手工管理内存的语言(比如 C 或 C++)转换到具有垃圾回收功能的比如 Java 语言时,我们的工作会变得更容易,因为当你用完对象后,会进行自动的垃圾回收。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情,其实不然

内存泄漏来源1-无意识的对象保持

先提供书中案例:

/**
 * 简单的栈实现
 *
 * @author misury
 **/
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];
    }
    
    /**
     * 修改后的版本
     * @return
     */
    public Object popVersion2(){
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
    

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

书中阐述这段程序并没有很明显的错误。无论如何测试,都会成功通过每一项测试,但是这个程序隐藏着一个**“内存泄漏”**问题。随着垃圾回收器活动的增加,或由于内存占用不断增加,程序性能降低逐渐表现出来。

程序中哪里发生内存泄漏?如果一个栈先是增长,然后再收缩,那么,**栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。**因为栈内部维护着这些对象的过期引用(永远也不会再被解除的引用)。本例中,凡是在 elements 数组的“活动部分”之外的任何引用都是过期的。活动部分是指 elements 中下标小于 size 的那些元素。优化后的版本也在上面程序中:popVersion2()方法

本人对前面这段话并没有很理解,有明白的大佬求指点。所以找了下面这一段我认为和上面这段代码想表达意思相同的程序:

/**
 * 运行的JVM参数:-Xmx64M -Xms64M
 *
 * @author misury
 **/
public class MemoryDemo {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        // 1. 获取现在空闲内存
        System.out.println("Now JVM total free memory: " + runtime.freeMemory());
        ArrayList<String> list = new ArrayList<>(1000000);
        // 2. 获取创建了一个100w容量的list后的空闲内存
        System.out.println(runtime.freeMemory());
        for (int i = 0; i < 1000000; i++) {
            list.add(new String("Rubbish"));
        }
        // 3. 获取list里面装了100w个元素后的空闲内存
        System.out.println("Free memory after create 1000000 rubbish :" + runtime.freeMemory());
        // 4. 触发GC
        System.gc();
        try {
            Thread.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 5. 获取触发GC后的空闲内存
        System.out.println("使用list.clear方法前进行垃圾回收的效果: " +runtime.freeMemory());
        System.out.println("------------------");
        list.clear();
        System.gc();
        try {
            Thread.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("使用list.clear方法后进行垃圾回收的效果: " + runtime.freeMemory());
    }
}

运行结果:

Now JVM total free memory: 64519120
60324816
Free memory after create 1000000 rubbish :37701288
使用list.clear方法前进行垃圾回收的效果: 37786368
------------------
使用list.clear方法后进行垃圾回收的效果: 61661576

与书中案例不同,通过控制台输出的内容可以看出,在没有调用list.clear()时,进行垃圾回收,List添加进去的元素没有办法回收掉,从clear()入手可以看到,清除的时候会将list的数组元素置空,清除该引用就会被垃圾回收器回收掉

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄漏为“无意识的对象保持”更为恰当)。如果一个对象引用被无意识地保留起来,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象引用的所有其他对象。修改这类问题的方法:一旦对象引用已经过期,只需清空这些引用

清空过期引用的另一好处是,如果它们以后又被错误的接触引用,程序就会立即抛出NPE异常,而不是悄悄地错误运行下去。尽快地检测出程序中的错误总是有益的

当我们第一次被类似这样的问题困扰时,就会变得过分小心:对于每一个对象引用,一旦程序不再用到它,就把它清空。**一朝被蛇咬,十年怕井绳。**其实没必要,清空对象引用应该是一种例外,而不是一种规范行为。

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

内存泄漏来源2 - 缓存

内存泄漏的另一个常见来源是缓存。 一旦把对象引用放到缓存中,就很容易被遗忘掉,从而使得它不再有用之后很长一段时间仍然留在缓存中。如果你正好要实现这样的缓存:**只要在缓存之外存在某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;**缓存中的项过期后,它们就会被自动删除

更为常见的情况是“缓存项的生命周期是否有意义”不容易确定,随着时间推移,其中项会变得越来越没价值。这种情况,缓存应该时不时清除掉没用的内容。清除工作可以由一个后台线程来完成,或也可以在给缓存添加新条目的时候顺便清理。LinkedHashMap利用它的removeEldestEntry方法可以很容易实现后一种方案

内存泄漏来源3 - 监听器和其他回调

如果你实现了一个 API,客户端在这个 API 中注册回调,却没有显式地取消注册。那么除非你采取某些动作,否则它们会不断堆积。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。例如:只讲它们保存成WeakHashMap中的键。

由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或借助于Heap分析工具才能发现内存泄漏问题。因此,如果能在内存泄漏发生之前就知道如何预测此类问题,阻止它们发生就最好不过了。