Java:Effective java学习笔记之 消除过期对象引用

Java消除过期对象引用

消除过期对象引用

很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的,所谓的垃圾回收GC会自动管理内存的回收,而不需要程序员每次都手动释放内存,但是如果存在大量的临时对象在不需要使用时并没有取消对它们的引用,就会吞噬掉大量的内存,很快就会造成内存溢出。

1、Java的垃圾回收机制

Java中的对象是在堆中分配,对象的创建有2中方式:new或者反射。对象的回收是通过垃圾收集器,JVM的垃圾收集器简化了程序员的工作,但是却加重了JVM的工作,这是Java程序运行稍慢的原因之一,因为GC为了能正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都要进行监控,监控对象的状态是为了更加准确、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

2、Java中的内存泄露

内存泄露的对象有以下两个特点:

  • ① 这些对象是可达的,即在有向图中存在通路可以与其相连。
  • ② 这些对象是无用的,即程序以后都不会再使用这些对象。

举例

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

以上是一个简单的Stack模拟。如果你测试,你会发现任何操作都是可以的。但是却有个不足:当取出栈顶数据的时候,栈顶数据未清空,使得GC不能够回收栈顶空间。如此反复插入、取数据,最终可能导致OutOfMemoryError。

如何修改呢?

很简单,将栈顶元素置空即可。即:

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();

    Object result = elements[--size];
    //消除过期对象引用
    elements[size] = null;
    return result;
}

那什么时候应该清空引用呢?

一般而言,只要类是自己管理内存,程序员就应该警惕内存泄露问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。本例中的Stack类就是自己管理内存的。存储池包含了elements数组(对象引用单元,而不是对象本身)的元素,数组“活动部分(下标小于size的那些元素)”中的元素是已经分配的,而其他部分的元素则是*的。但是对于垃圾回收器来说所有对象引用都是一样有效的。一旦数组元素变成了非活动部分的一部分,程序员就手工清空这些数组元素。

是不是有点疑惑?

以后我们在开发中是不是都需要将无需使用的对象置空?答案是否。置空对象应该是例外情况,而不能作为规范,否则GC就是多余的了。

其实,当类需要自己管理本身的内存时,才需要消除过期对象的引用。开发者应该警惕内存泄漏问题。通常这样的类常常与数组相关联,开始时数组中都存储了数据,之后取出(不可用),但是GC并不明白此时的取出操作,因为数据还是存储在内存中的,只有开发者知道此时的取出就是使数据不可用,所以开发者需要给GC提示。

3、常见的内存泄露

  • 第一种:类是自己管理内存

一般而言,只要类是自己管理内存,程序员就应该警惕内存泄露问题。

解决方法:一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

  • 第二种:缓存

一旦你把对象引用放到缓存中,它很容易被遗忘掉,从而使得它在没有用之后很长一段时间仍然保存在缓存中。

解决方法:

(1)当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,也就是说在缓存之外存在对某个项的键的引用,改项才有意义,就可以使用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会自动被删除。

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

  • 第三种:监听器和其他回调

比如你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,那么除非你采取某些动作,否则他们就会积聚。

解决方法:确保回调立即被当做垃圾回收的最佳方法是只保存他们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。

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

  • 第四种:使用连接池时

除了要显式地关闭连接,还必须显式地关闭Resultset和Statement对象。否则会造成大量的这些对象无法释放,从而引起内存泄露。

参考

上一篇:Selenium(11):通过find_elements定位一组元素


下一篇:0347-leetcode算法实现之前K个高频元素-top-k-frequent-elements-python&golang实现