2008年6月1日星期日

Java内存泄漏初探(1)

进来编写的一个程序在长时间运行后似乎会停止响应,想到了是否是内存泄漏的缘故。之前没有好好研究过Java内存泄漏的问题,所以首先搜索了一下网络,结合Jprofiler工具,想先对Java Application的内存泄漏问题作一下研究。

通过搜索,网上所举的关于内存泄漏的问题其中一个为:

释放不用的对象:参见《java内存泄漏》,了解JVM的工作原理及机制规范;
ArrayList al = new ArrayList();
for(int i = 0;i<200;i++){
Object o = new Object();
o.name = 1;
o.id = 1;
al.add(o);
}
以上代码中o仍被al引用,没有释放,不会被回收。因此需修正:
Arraylist al = new Arraylist();
for(int i = 0;i<200;i++){
Object o = new Object();
o.name = 1;
o.id = 1;
al.add(o);
o = null;
}

注意上面红色字体部分是错误的。而修正中的 o=null; 更是画蛇添足。因为o本来就是一个局部变量,每次循环时都会去引用一个新的对象,而且再离开复合语句时o就不存在了,但是由于对象被加入了集合ArrayList中,因此对象还是被引用的,并不会被回收。

为了在JProfiler中查看对象分配和回收的情况,我写了下面的两个类

package my;

public class MyObject {
    public int name;
    public int id;
}

package my;

import java.io.IOException;
import java.util.ArrayList;

public class Test2 {
    public static void main(String[] args) throws IOException {
        char ch = ' ';
        while (ch != 'y')
            ch = (char) System.in.read();
        ArrayList<MyObject> al = new ArrayList<MyObject>();
        for (int i = 0; i < 200; i++) {
            MyObject o = new MyObject();
            o.name = 1;
            o.id = 1;
            al.add(o);
           o = null;
        }
        ch = ' ';
        while (ch != 'n')
            ch = (char) System.in.read();
        al=null;
        ch = ' ';
        while (ch != 'n')
            ch = (char) System.in.read();
    }
}

经过分析,程序在运行时,上述代码中蓝色部分 o=null; 加与不加,对对象的分配和回收是没有任何影响的。而红色的 al=null; 如果不加的话,那么在程序中在这一点明显可以看到内存是不能回收的,但是,由于al本身是一个局部变量,因此在main()运行结束时,al的生命周期结束,它所引用的对象都会被释放。

江南白衣所写的《编写对GC友好,又不泄漏的代码》 (最新版链接:http://blog.csdn.net/calvinxiu/archive/2007/05/22/1621051.aspx)中,提到了这样几条原则:

1.使用更多生命周期短的、小的、不改变指向(immutable)的对象,编写清晰的代码。

    出于懒惰也好,朴素的节俭意识也好,我们都习惯对一个变量重用再重用。但是....

  • Java的垃圾收集器喜欢短生命周期的对象,对象如果在新生代内,在垃圾收集发生前就死掉了,垃圾收集器就什么都不用做了。
  • 现代JVM构建一个新对象只需要10个本地CPU指令,并不弱于C/C++。 (但垃圾收集没有压缩算法时会稍慢,更频繁的New对象也导致更频繁的GC)。
  • 大对象的分配效率更低,而且对非压缩算法的垃圾收集器,更容易造成碎片。
  • 对象重用增加了代码的复杂度,降低了可读性。

   所以有标题的呼吁,比如不要害怕为中间结果分配小对象。但编程习惯的改变也不是一朝一夕的事情。

2.将用完的对象设为NULL其实没什么作用。

    貌似很酷的把对象主动设为Null 的"好习惯"其实没什么用,JIT Compiler会自动分析local变量的生命周期。
    只有一个例外情况,就是String[1024] foo 这种赤裸裸的数组,你需要主动的foo[100]=null释放第100号元素,所以最好还是直接用ArrayList这些标准库算了。

3.避免显式GC--System.gc()。

    大家都知道System.gc()不好,full-gc浪费巨大,gc的时机把握不一定对等等,甚至有-XX:+DisableExplicitGC的JVM参数来禁止它。

    哈哈,但我还不会用System.gc()呢,不怕不怕。真的不怕吗?

  • 先用FindBugs 查一下所用到的全部第三方类库吧...
  • 至少RMI 就会老实不客气的执行System.gc()来实现分布式GC算法。但我也不会用RMI啊。那EJB呢,EJB可是建在RMI上的....

    如果无可避免,用-Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 (单位为微妙) 增大大GC的间隔(原默认值为1分钟),-XX:+ExplicitGCInvokesConcurrent 让System.gc() 也CMS并发执行。

4.继续千夫所指的finalize()

    大家也都知道finalize()不好,分配代价昂贵,释放代价更昂贵(要多走一个循环,而且他们死得慢,和他们相关联的对象也跟着死得慢了),又不确定能否被调用(JVM开始关闭时,就不会再进行垃圾收集),又不确定何时被调用(GC时间不定,即使system.gc()也只是提醒而不是强迫GC,又不确定以什么样的顺序调用,所以finalize不是C++的析构函数,也不像C++的析构函数。

   我们都知道啊,所以我从来都没使用。都是在显式的维护那些外部资源,比如在finally{}里释放。

在上面的例子中,确实也如白衣的文章所述,一般情况下没有必要将对象设置为null。

另外,一些文章中提到了使用ArrayList的方法clear()来清除ArrayList对对象的引用,该方法的源代码如下,实际上就是将其中的每个引用变量设置为null,断开对对象的引用。

    /**
     * Removes all of the elements from this list.  The list will
     * be empty after this call returns.
     */
    public void clear() {
    modCount++;

    // Let gc do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
    }

而前面的代码已经说明,对ArrayList对象消亡的时候,所引用的对象也会被回收,所以这个clear方法一般不需要调用。

1 条评论:

  1. 使用更多生命周期短的、小的、不改变指向(immutable)的对象,编写清晰的代码--------------欣赏。

    回复删除