java程序性能优化之找出内存溢出元凶

83951137 2009-06-08

我曾经在刚入行的时候做过一个小的swing程序,用到了javaSE,swing,Thread等东东,当初经验少也没有做过严格的性能测试,布到生产环境用了一段时间后发现那个小程序有时候会抛java.lang.OutOfMemoryError异常,就是java的内存溢出。当时也上网查了不少资料,试过一些办法,代码也稍微做了些优化,但是有一个问题我始终是找不到解决的方案-不知为什么子窗体关闭后java的垃圾回收机制无法回收其资源,因为这个程序可能要经常开关一些子窗体,那么这些子窗体关闭后无法释放资源就造成了程序OutOfMemoryError的潜在的隐患!

最近无意间在网上看到了一个监控java程序内存使用的工具-JProbe,马上回想起那个未解决的难题,于是我就下载了JProbe8.0.0希望从分析内存入手找到我要的答案。软件下载安装后,在安装目录里详尽的用户指南(懂点软件和英语的人很快就能上手),主要是两个步骤:

1.用JProbeConfig工具根据提示生成J2SE或者J2EE程序的控制脚本(一个.jpl文件和一个.bat文件),在命令行里进入.bat文件所在的目录,然后执行该批处理让要监控的java程序跑起来

2.运行JProbeConsole工具,点击“AttachtoSession...”按钮,弹出java程序的内存实时监控图表“RuntimeSummary”,我们主要是看“Data”卡片里的内容(注意:第一次使用该软件可能会遇到一些小问题:比如发布为jar包的程序如果运行时会去读配置文件,从控制脚本启动的话,可能会发生配置文件找不到的异常,解决办法是:不要打jar包,直接就用文件夹发布;还有可能因为一些杀毒软件的网络防火墙导致JProbe无法连接到控制脚本的session,造成监控图表打不开,解决办法是:取消防火墙对于JProbe访问网络的限制)

实时监控图表“RuntimeSummary”如下图所示:

可以设置要监视的包或者类,然后点击“RefreshRuntimeData”按钮刷新这些对象占用内存的情况,当你觉得某个类比较可疑的话,你可以在不断的使用程序的过程中监视它的生命周期,看看它是否像预期的那样在结束了生命周期后占用的内存就被释放。众所周知:java的内存回收是自动进行的,无需程序员干预,我们称其为垃圾回收,这个垃圾回收可能是不定期的,就是当程序占用内存资源比较少的情况下可能jvm的垃圾回收频率就比较低;反之,java程序消耗内存资源比较多的情况下,垃圾回收的频率和力度就比较高,这种垃圾回收的不确定性很可能会影响我们的判断,但我们可以点击JProbe监控界面右上方的“RequestaGarbageCollection”(像一个垃圾桶的图标)按钮来向jvm发出垃圾回收的请求,等几秒后再去点击“RefreshRuntimeData”,这个时候如果那个预期应该已经销毁的对象的类名还是没有从监控界面下方的class列中消失或者其对象数量没有减少的话(请多试几次,中间可以夹杂些其他增加程序内存使用的操作以确保jvm确实执行了垃圾回收),那恭喜你!90%的可能性你已经找到了程序的某个缺陷

这个查找元凶的过程可能是相当耗时的,是对程序员的耐心的挑战。我熬了一下午一晚上,功夫不负有心人,基本上把我那个小程序的所有内存溢出的漏洞都找到并补上了。事实告诉我之前那个子窗体关闭后资源无法释放的根本原因是:子窗体虽然调用了dispose()方法,但是子窗体对象的引用一直都在:或者是被静态HashMap引用、或者是它的内部子线程类没有释放、或者是它的某个事件监听类没有释放(借用JProbe的火眼金睛一检验,发现问题真是一大堆啊!),so.我们要彻底释放某个对象占用资源的关键在于找到并释放所有对它的引用!

下面是我解决具体问题的一些经验:

程序中造成内存溢出可能性最大的是HashMap,Hashtable等等集合类,尤其是静态的,更是要慎之又慎!!!它们引用的对象可能你感觉已经销毁了,其实很可能你忘记remove键值,而如果这些集合对象还是静态的挂在其他类里面,那么这个引用可能一直都在,借用JProbe测试一下,结果往往出人意料,解决办法:彻底删除键,remove、clear,如果允许最好把集合对象设为null

对于不再使用的线程对象,如果要彻底杀了它,很多书上都推荐用join方法,我之前也是这样做的,但后来借助JProbe工具我吃惊的发现这样做很可能要杀的线程仍旧好好的活在你日益增大的内存里,很可能调用了线程的sleep方法后用join方法就会有点问题,解决办法:在join方法前再加一句执行interrupt方法,不过这个时候可能会有新的问题:执行interrupt方法后你的线程类会抛InterruptedException,上有政策下有对策,加一个开关变量做判断就能完美解决,可参考下面的代码:

/**
     * <p>Description: 创建线程的内部类</p>
     * @author cuishen
     * @version 1.1
     */
    class NewThread implements Runnable {
        Thread t;
        NewThread() {
            t = new Thread(this, path);
            t.start();
        }
        public void run() {
            try {
                while(isThreadAlive) {
                    startMonitor();
                    Thread.sleep(Long.parseLong(controlList.get(controlList.size() - 1).toString()));
                }
            } catch (InterruptedException e) {
            	if(!ifForceInterruptThread) {//开关变量
            	    stopThread(logThread);
                    String error = "InterruptedException!!!" + path + ": Interrupted,线程异常终止!程序已试图重启该线程!!";
                    System.err.println(error);
                    LogService.writeLog(error);
                    createLogThread();
            	}
            }
        }
    }

    public void createLogThread() {
    	ifForceInterruptThread = false;//开关变量
        logThread = new NewThread();
    }

    private void stopThread(NewThread thread) {
        try {
        	thread.t.join(100);
        } catch (InterruptedException ex) {
            System.out.println("线程终止异常!!!");
        } finally {
        	thread = null;
        }
    }

    /**
     * 关闭并彻底释放该线程资源的方法
     */
    public void stopThread() {
        try {
        	ifForceInterruptThread = true;//开关变量
        	isThreadAlive = false;
        	logThread.t.interrupt();
        	logThread.t.join(50);
        } catch (InterruptedException ex) {
            System.out.println("线程终止异常!!!");
        } finally {
        	this.controlList = null;
        	this.keyList = null;
        	logThread = null;
        }
    }

对于继承JFrame的窗体类,我们要注意在初始化方法中加入:this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);,并且注意和其关联的事件监听类一律写成窗体类的内部类,这样窗体dispose()的时候,这些内部类也一并销毁,就不会再有什么莫名其妙的引用了

相关推荐