JAVA界面内存泄漏问题
问题描述
EMF界面项目组使用JAVA做为开发工具,进行基于SWING/AWT的界面开发工作。在EMF界面项目组的集成测试过程中,发现前台系统每次注销后重新登录,都会导致程序占用的内存上涨10~20M,导致在内存为256M的DELL PC上进行5~6次注销操作后,系统内存被耗尽,程序抛出java.lang.OutOfMemoryError异常退出。
根本原因分析
a) gc的机制
Java程序员基本上都知道,使用像 Java 这样的编程语言的一大特点就是,不必再担心内存的分配和释放问题。只须创建对象,当应用程序不再需要这些对象时,Java 会通过一种称为“垃圾收集”的机制将这些对象删除。这种处理机制是否就意味着不会有内存泄露问题或者说就不需要管理内存释放问题呢,看一下垃圾回收器的工作原理:
垃圾收集的工作方式:圾收集器的工作是发现应用程序不再需要的对象,并在这些对象不再被访问或引用时将它们删除。垃圾收集器从根节点(在 Java 应用程序的整个生存周期内始终存在的那些类)开始,遍历被引用的所有节点进行清除。在它遍历这些节点的同时,它跟踪哪些对象当前正被引用着。任何类只要不再被引用,它就符合垃圾收集的条件。当删除这些对象以后,就可将它们所占用的内存资源返回给 Java 虚拟机 (JVM)。
所以的确是这样,Java 不要求程序员负责内存的管理和清除,它会自动对无用的对象执行垃圾收集。
上述说明了以下2点:
1、对于无用对象,gc的确负责回收
2、对于实际上无用,而还被引用的对象,gc就无能为力了(事实上,gc认为它仍旧有用)。而这一点就是导致内存泄露最重要的原因
b) 对象的有用和无用
程序中的对象是否有用和Java gc认为对象是否有用是有差别的:
1、程序员编写代码通常是认为被创建的对象在其生命周期结束后无用
2、而gc 认为只有对象的引用记数=0 的时候,该对象才是无用的
两者会产生不一致的地方,如下图所示:
代码(程序员的观点) | gc(Java ) |
Public void fun1() { .... //创建局部变量E Object E = new E(); A.a = E; B.b = E C.c = E; D.d =E;
// 我们认为 // E 没用了,释放E E = null .... } |
E.count ++ E.count ++ E.count ++ E.count ++ E.count ++
E.count-- |
认为已无用 | E的应用数=4 仍旧有用 |
应该释放 | gc不负责释放 |
结论:1、如果要释放对象,就必须使其的引用计数为0,只有那些不再被引用的对象才能被释放,这个原理很简单,但是很重要,是导致内存泄露的基本原因,也是解决内存泄露方法的宗旨。
2、程序员无须管对象空间具体的分配和释放过程,但必须要关注被注释放对象的引用计数是否为0
C) 一个对象可能被其他对象引用的过程
在代码编写过程中,一个对象很可能被其它对象引用,其中的引用过程基本有以下几种:
1、直接赋值,如上例中的A.a = E
2、 通过参数传递:例如 public void addObject(Object E)
3、其它一些情况如系统调用等
d) 几种容易遗忘并且导致不能释放的引用情况
1)、通常的无用引用
上面说明了在 Java 应用程序执行期间具有不同生存周期的两个类。类 A 首先被实例化,并会在很长一段时间或程序的整个生存期内存在。在某个时候,类 B 被创建,类 A 添加对这个新创建的类的一个引用。现在,我们假定类 B 是某个用户界面小部件,它由用户显示甚至解除。如果没有清除类 A 对 B 的引用,则即便不再需要类 B,并且即便在执行下一个垃圾收集周期以后,类 B 仍将存在并占用内存空间。
2)、内部类的引用
内部类的引用是比较容易遗忘的一种,而且一旦没有释放可能就导致一系列的后继类对象没有释放。从内部类的引用来看我们要释放对象A,需要做到的不仅仅是将对象A的引用计数清为0,最好是将指向A对象以及A对象内部成员的引用都清为0。
3)、监听器引用
在Java 的编程中,我们都需要和监听器打交道,通常一个应用当中会运用到很多的监听器,我们会调用一个控件的诸如AddXXXListener()等方法来增加监听器。建议在释放对象的时候删除这些对象,如果不这样做,那么程序存在内存泄露的将增大很多。
4)、外部模块的引用
对于程序员而言,自己的程序很清楚;如果发现内存泄露,自己对这些对象的引用可以很快定位并解决。但是现在的应用软件并非有一个人实现,模块化的思想在现代软件中非常明显。所以程序员要小心外部模块不经意的引用,例如程序员A负责A模块,它调用了B模块的一个方法如:
Public void registerMsg(Object b);
这种调用就要小心了,你传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B是否提供相应的操作去除引用。
5)、系统的引用
这种引用比较少,但很难定位和解决,类似监听器引用,当创建系统类对象,譬如线程、定时器、面板、颜色选取对话框、文件选择对话框等等。可能会产生一些引用,导致和它们相关的类不能释放。对于其中的原因和解决方案,大家可以去研究。在IManager N2000EMF UI中已经总结了一些,可参考解决方案这一节。
结论、解决方案及效果
工作步骤:
1、选择内存测试工具(N2000EMF UI使用了JProbe)
2、定位存在内存泄露的地方,找出那些应该释放而没有释放的对象
3、根据相应的现象修改代码,关键释放无用引用
4、重新测试修改后的代码。
Jprobe和内存泄露查看:
1、关于Jprobe的配置,请查看相应的手册
2、运行程序到达稳定状态,如下:
当系统到达稳定状态时,在图中绿线的地方插入一个检查点,然后作了一次要检查的操作(比如打开一个窗口)这里是登陆了一次,可以看到堆内存明显上升,然后注销了一次,相当于关闭窗口。堆内存就有一定程度的下降,但没有回到登陆之前的内存使用量,这说明可能在(登陆、注销)的这一对操作中,存在着一些对象没有被释放。如果反复这一对操作,发现内存的使用量不断增长,那么在这个过程中必定存在内存泄露。
很明显,登陆时分配了内存,在注销的时候没有被释放。导致了堆内存不能回到登陆前的状态。通过这中操作可以发现你的每一种操作,譬如打开一个窗口,然后关闭它,会不会存在内存泄露。
另外:如果存在内存泄露,要查看Instance Summary表,这个表里显示了当前堆内每种对象的实例数。查看这些数量的变化以及内存的变化,会发现究竟那些类对象没有被释放。
总之:通过工具可以查看到一次操作中,哪些需要释放的对象没有被释放,还能基本上定位那些引用还指向这些对象。具体的关于JProbe的用法请参考相关手册。
顺序和效率
当发现有很多类对象没有被释放的时候,不要急于的解决这些问题,要分析这些类对象的关系。举例如下:
如果你只要释放B圈内的对象,那么最好从A1开始释放,首先关注A1的释放情况,如果你要释放C圈内的对象,那么最好从A对象的释放开始做起。这和Java GC的工作机制一样,就可以提高我们解决内存泄露问题的效率。
代码修改的原则:
如果要释放一个对象,就要将指向该对象的引用清空,最好连带指向该对象内部的引用也清空。
例如:要释放类对象指向图中A4对象,就要释放A1->A4的引用和A2->A4的引用
经验总结、预防措施和规范建议
1、容器(Panel,Dialog等)的removeAll()方法
当类是从JPanel或JDialog类或其它容器类继承的时候,删除该类对象之前不妨调用它的removeAll()方法。
2、线程的interrupt()方法
当类对象是一个Thread的时候,删除该类对象之前不妨调用它的interrupt()方法。
3、调用Timer 和TimerTask的Cancel()方法
4、JFileChooser的removeChoosableFileFilter()
如果创建了一个JFileChooser,并且加入了自己的文件过滤器,那么删除该类对象之前需调用它的removeChoosableFileFilter()方法,例如:
// 防止内存泄漏
chooser.setFileFilter(txtFilter);
chooser.removeChoosableFileFilter(chooser.getFileFilter());
chooser.setFileFilter(htmlFilter);
chooser.removeChoosableFileFilter(chooser.getFileFilter());
chooser.setFileFilter(csvFilter);
Chooser.removeChoosableFileFilter(chooser.getFileFilter());
5、当不需要一个类对象,最好删除它的监听器,以防内存泄露
6、内存检测过程中不仅要关注自己编写的类对象,同时也要关注一些基本类型的对象例如:int[],String ,char[]等等。
7、在确认一个对象无用后,将其所有引用显式的置为NULL。