消除过期的引用对象

C或者C++是手工管理内存语言,java语言是自带垃圾回收,程序员的工作就很容易,因为当你用完了对象之后,他会被自动回收。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实是不对的。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAUT_INITIAL_CAPACITY = 16;


    public Stack() {
        elements = new Object[DEFAUT_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);
    }
}

这段代码无论怎么测试,都会成功通过每个测试,但是程序中有一个隐藏的问题。不严格的讲这段程序有一个“”内存泄漏“”,随着垃圾回收器活动增加,或者内存占用不断增加,程序的性能降低。在极端情况下,内存泄漏会导致磁盘交换,甚至程序失败( OutOfMemoryError)。

那么程序哪里会发生内存泄漏呢?如果一个栈现实增长然后在减小,那么从栈中弹出来的对象不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为栈内部维护着对这些对象的过期引用。所谓的过期引用,是指永远也不会被解除引用。支持垃圾回收的语言中,内存泄漏时很隐蔽的。如果一个对象引用被无意识的保留下来,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的其他对象。即使只有少量的几个对象被无意识的保留下来,也会有需要对象被排除在垃圾回收机制之外,从而对性能造成重大影响。

这类问题修复很简单:一但对象过期就可以清空。对于Stack类而言,只要一个单元被弹出,指向它的引用就过期。pop方法修改
版本

public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object o = elements[--size];
        elements[size] = null;
        return o;
    }

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

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

那么,何时应该清空引用呢? Stack 类的哪方面特性使它易于遭受内存泄漏的影响呢 ?简而言之,问题在于, Stack 类自己管理内存存储池( storage pool )
包含了elements 组(对象引用单元,而不是对象本身)的元素 。数组活动区域(同前面的定义)中的元素是己分配的( allocated ),而数组其余部分的元素
则是自由的( free ),但是垃圾回收器并不知道 这一点;对于垃圾回收器而言, elements 数组中的所有对象引用都同等有效,只有程序员知道数组的非活动部分
是不重要的, 程序员可以把这个情况告知垃圾回收器,做法很简单:一 旦数组元素变成了非活动部分的一部分,程序员就手工清空这些数组元素。

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

内存泄漏的另一个常见来源是缓存 一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中,对于这个
问题,有几种可能的解决方案 如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用, 该项就有意义,那么就可以用 WeakHashMap
代表缓存;当缓存中的项过期之后,它们就会自动被删除,记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定 时, WeakHashMap 才有用处

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

内存泄漏的第三个常见来源是监昕器和其他回调 ,如果你实现了一个API,客户端在这个 API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,
否则它们就会不断堆积起来。 确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用 weakreference ,例如,只将它们保存成 WeakHashMap 中的键

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

转载于:https://www.jianshu.com/p/37c99b7a7d73

猜你喜欢

转载自blog.csdn.net/weixin_33704591/article/details/91278895