理解Java内存泄露

今天在看Effective Java 第三版的时候,看到第7条 Eliminate obsolete object references。发现即使书中的Java版本已经到了Java9,仍然认为将不活跃的数组元素设置为null可以让GC发现,并进行垃圾回收。原以为作者犯错了(差点破口大骂),后来发现是我理解错了作者的意思。主要原因是我没有真正理解Java里的内存泄露。

Java的内存泄露

Java的内存泄露和C/C++的内存泄露意义不一样。

C/C++中的内存泄露表示指针丢失了,而无法再找到那块开辟的内存了,而且C/C++是没有自动垃圾回收的,所以之后再也无法释放那块内存了,从而导致内存不可用。

Java中即使有自动垃圾回收机制,仍然有内存泄露。只是概念和C/C++不同。Java的内存泄露主要体现在:当我们不再需要使用那块内存中的对象时,却仍然有其他地方持有指向那块内存的引用,从而导致垃圾收集器不去回收那块内存,从而导致一直有对象在占用内存,最终内存不可用。

注意他们的区别,C/C++主要是“遥控器”都丢失了,从而无法释放内存,而Java是引用仍然在,程序仍然可以访问那块内存,实际上程序已经不需要使用那些对象了,理应回收,但由于其他地方持有对象的引用,导致垃圾收集器不去回收内存。

深入一点

举个例子,下面是自己写的一个stack的实现(很粗糙):

public class MyStack<E> {

    private Object[] elements;

    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public MyStack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

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

    public boolean empty() {
        return size == 0;
    }

    @SuppressWarnings("unchecked")
    public E pop() {
        E e =  (E) elements[--size];
        return e;
    }
}

这里看似没有什么毛病,却隐藏着“内存泄露”的问题。为什么呢?我们可以写一个测试类来看看。

public class Main {


    public static void main(String[] args)  {
        MyStack<Long> myStack = new MyStack<>();
        for (long i = 0; i < 10000000; i++) {
            myStack.push(i);
        }

        System.out.println("开始pop");
        int i = 0;
        while (!myStack.empty()) {
            myStack.pop();
            i++;
        }

        System.out.println(i);
        while (true) {
          //仅仅是为了不让程序那么快就结束。
        }

    }
}

运行测试类,打开jconsole来看看内存的信息,(jconsole是JDK里的工具,在命令行输入jconsole即可),可以看到类似下图所示的信息。

jconsole监控内存

注意这里堆内存的使用量一直在大概300多M左右。根据我们的代码,程序这时候应该只是在空转(while循环),并没有创建对象的过程,而且我们之前已经用pop操作将对象弹出栈了,表明这些对象已经没用了,应该被回收才是。那为什么始终没有回收呢?大家可以尝试点一下执行GC的按钮,会发现仅仅回收了Eden区的内存,Old区的点多少次都不会回收。

这是因为其实stack对象里的数组仍然保持对这些对象的引用,只要stack对象还有引用指向它,会一直保持下去。但是实际上,我们已经不需要它们了,不是吗?(已经pop了)那么该如何解决呢?其实解决方案非常简单,只需修改pop方法即可。如下所示:

    @SuppressWarnings("unchecked")
    public E pop() {
        E e =  (E) elements[--size];
        elements[size] = null;       //仅仅多了这一行代码
        return e;
    }

在很多文章里,都说现代的Java里,将一个引用指向null对GC也许并没有什么帮助,大多数时候只是为了防止对象再次被使用而已。但当我们加入这行代码,再次运行测试类,再次打开jconsole就可以发现,这次确实发生了GC,stack里被pop的对象内存被释放了!!!

在上面的测试类下,GC不会自动发生(除非设置内存很小),需要自己点击执行GC,强制来一次GC,才会释放内存。这是因为在到达while()循环之前,这些对象经历过一定GC次数,已经被放入老年代,老年代不会轻易被回收,一般只有内存不够的情况下回发生GC。不过至少点了“执行GC”,发现内存确实被回收了,是吧。

为什么那短短一行代码的力量那么强大呢?在《Effective java 3rd》里有那么一段话:

The garbage collector has no way of knowing this; to the garbage collector, all of the object references in the elements array are equally valid.Only the programmer knows that the inactive portion of the array is unimportant.The programmer effectively communicates this fact to the garbage collector by manually nulling out array elements as soon as they become part of the inactive

大致意思是垃圾收集器也无法知道该不该回收那块内存,因为数组仍然保持着对对象的引用。只有程序员知道这些是不需要的对象,程序员应该告诉垃圾收集器这个事实。(在我们的程序里,通过设置对不要的对象引用为null来达到这个目的)

为什么将引用设置为null可以使得垃圾收集器知道该引用指向的对象已经不需要了呢?

以下是个人观点,如有错误,望指正:
网上很多文章或者网友讨论说将引用设置为null很多时候只是编码习惯,也是为了防止其他地方误用不需要的对象(会发生NullPointer异常)。但其实这只是一个方面的作用,另一个作用是表明这个对象已经没用了,该对象如果没有其他引用的话,就会处于“游离”状态,垃圾收集器通过可达性分析就可以知道这个对象可以被回收了,最终将其回收。

上述提到的设置为null并没有什么特殊效果也是分场景的,例如Stack这种场景,即使pop了也并没有消除引用,设置null就可以发挥作用。其他的例如一些局部变量,因为离开了作用域就没用了,垃圾收集器可以发现这点,所以null就没有发挥什么作用。

其他常见的内存泄露

缓存

缓存是很有可能导致内存泄露的,想想web开发中常常用到Redis做缓存,往往要给缓存设置一个超时时间,超时则从Redis里移除该缓存。有没有想过,为什么要设置这个超时时间?就是因为缓存一直在占用内存,而且我们无法确定该缓存是否还有用(有可能之后再也不会访问了,也有可能被频繁访问),如果不设置超时时间,随着时间的推移,缓存越来越多,占用的内存也越来越多,最终会导致内存不足,甚至整个系统因此而崩溃。

小结

自动垃圾回收机制并代表绝对不会发生内存泄露,相反,有垃圾回收机制的语言,发生内存泄露的地方更加“隐蔽”,难以发现。使用一些检测内存泄露的工具可以定位和排除问题,例如jstat ,jconsole等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,839评论 18 399
  • Android 内存管理的目的 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。简单粗...
    晨光光阅读 1,329评论 1 4
  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供...
    简欲明心阅读 90,050评论 17 311
  • 一 最近老是写点儿有的没的,因为确实心情不太好。 嘛,总有回复过来的一天。 最近大一的孩子们都在军训,那今天来写一...
    独木Atree阅读 562评论 0 1
  • 走过一个春天 脚印是潮湿的 路上有洼地 有细絮的风声 有逐渐疯长起来的野草地 有已经飞远的鹧鸪 我的记忆如烟 我学...
    爱亦如诗阅读 139评论 0 2