创建和销毁对象-消除过期对象引用

303 阅读4分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

警惕内存泄漏

Java 这种具有垃圾回收的语言,可以帮助我们使用完对象后,自动回收对象。你可能觉得自己不用考虑内存管理这些事,有JVM的垃圾回收就够了,其实并不是这样 。在你不知道的时候可能内存泄露就存在了。 那么在什么时候有可能出现内存溢出呢?

关注Stack,List等“数据容器”中的数据元素

思考一下下面这个简单的栈实现的例子:

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.copyOf(elements, 2 * size + 1);
    }
}

我们来调用一下这个程序

    public static void main(String[] args) throws InterruptedException {
        Stack stack = new Stack();
        int i;
        for(i = 0; i < 1000; ++i) {
            User myClass = new User(i, "名字是" + i);
            stack.push(myClass);
        }
        Thread.sleep(15000L);
        for(i = 0; i < 500; ++i) {
            stack.pop();
        }
        Thread.sleep(1000000L);
    }

image.png 可以看到在第一次sleep之前,User被新建了1000个,但是Integer同时也被添加了一些,却不是1000个,而是872个,这个的原因就是我们在讲Integer变量池的时候说到的,因为1-128已经被变量池创建了,所以他们不需要重新创建了!而我们为什么会多出来这么多的Integer的原因是因为自动装箱,也就是避免创建不必要的对象这篇文章中讲到的原因,User的构造器自动给我们创建的这些对象。。。

image.png

如果一个stack先增后减,那么会弹出的对象并不会被当成垃圾回收,原因在于stack内部维持着对这些对象的过期引用(在这里是数组的原因),这样就造成了内存泄漏。上面的程序到最后就是在出栈之后,仍然有1000个元素,没有回收掉对应的出栈的500元素,就是这个原因。

因此,在弹出数据元素的时,最好是释放元素引用,如:

Object obj = elements[--size];
elements[size] = null

在进行栈对象的出栈操作之后,对应数组的对这些对象的引用就应该被干掉,只有这样,才能让对应内存的 一般而言,无论任何时候一个类自己管理内存,程序员都应该警惕内存泄漏 。一个元素无论任何时候被释放,该元素中包含的任何对象引用都应当被清空。

注意缓存

一旦你把一个对象引用放入缓存中,它就会很容易被遗忘掉,并且在它变得无关紧要后很长一段时间内仍然留在缓存中。对于这个问题有多个解决方案。如果你足够幸运的话 ,只要在缓存之外有对某个项的键(key)的引用,就可以实现与缓存项(entry)完全相关的缓存,那么就可以用 WeakHashMap 代替。在缓存中的项过期后,它们将会被自动清除。记住只有当所要的缓存项的生命周期是由该键的外部引用决定而不是值来决定时,WeakHashMap 才是有用的。

注意监听器(listener)和其它回调(callback)

如果你实现了一个 API,客户端在这个 API 中注册回调但未明确撤销它们,那么除非采取一些措施,否则它们就会累积。确保回调被及时垃圾回收的一种方式,就是仅存储对它们的弱引用(weak reference) ,例如,仅将它们存储为 WeakHashMap 中的键。

由于内存泄漏通常不会表现为明显的错误,所以它们可能在一个系统中存在多年。往往只有通过仔细的代码检查或者借助被称为 heap profile 的调试工具才能发现。因此,学会在内存泄漏发生之前预测到这样的问题,并阻止它们发生,那是最好不过的了。