Java 程序里的内存泄漏(二)

2014-11-24 09:09:59 · 作者: · 浏览: 4
m)拥有着更多的物理内存。
为了讨论这个问题,我使用了 JProbe 来断定问题的存在。即使拥有 JProde 提供的强大工具和内存快照,调查仍然是一个繁琐的、反复的过程,它涉及先确定内存泄漏的原因,然后做出代码更改并验证其效果。
JProbe 有几个选项来控制在一次调试回话期间什么样的信息会被记录。经过一些试验后,我判定获取所需信息的最有效的方式是关掉性能数据收集,专注于捕获的堆数据。JProbe 提供了一个叫做运行时堆摘要的视图来显示 Java 应用程序在一段时间内使用的堆内存的数量。它同时也提供了一个工具栏按钮用来在需要时强制 JVM 执行垃圾收集 --在想要看一下一个类的给定实例不再为 Java 应用程序需要时是否会被垃圾收集,这个功能是很有用的。下图显示了在一段时间内使用的堆存储量。
运行时堆摘要

在堆使用情况图中,蓝色部分表示已分配的堆空间量。我启动 Java 程序之后它达到了一个稳定点,我强制垃圾收集器执行,这由绿线之前的蓝色曲线的一个骤降表示(这条绿线表示一个检查点被插入)。接下来,我先是添加而后删掉了四个表单并再次调用垃圾收集器。检查点之后的蓝色曲线的水平线比检查点之前的蓝色曲线的水平线高的事实告诉我们很可能出现了内存泄漏,因为该程序已经回归其只有一个简单可见的表单的初始状态。我检查实例确认了泄漏。总之,结果表明 FormFrame 类(表单的主 UI 类)的数量在检查点之后增加了四个。
寻找原因
要想将测试人员提交的问题隔离出来,第一步就是提供一些简单的、重复的测试用例。以上面那个例子为例,我发现简单地添加一个表单,删除这个表单,然后强制垃圾收集器的结果是一些关联到已经删除掉的表单的实例仍然存活着。这种问题通过 JProbe实例摘要视图来看是显而易见的,视图中统计了堆内存中每个类的实例的个数。
要定位垃圾收集器工作时具体实例的引用,我使用了 JProbe 的引用画面,如下图所示,来断定哪些类仍然在引用已被删除掉的 FormFrame 类。这是调试这种问题的巧妙地方法之一,我通过它发现了很多不同的对象仍然在引用那些无用的对象。而通过试错来查明究竟是哪个引用者真正造成这个问题的过程却是相当耗时的。
在这个案例中,根类(左上角红色的那个)是出现问题的起源。右侧用蓝色突出的那个类就是追踪到的 FormFrame 类。
使用引用画面跟踪内存泄漏

对于这个具体的例子,找到的罪魁祸首是一个包含一个静态的哈希表的字体管理类。通过引用列表追踪后,我发现根节点是一个静态的哈希表,这个哈希表保存了每个表单使用的字体。各种表单可以被独立地放大或缩小,所以哈希表包含了一个具有每个指定的表单的所有字体的向量。当表单的缩放视图改变时,带有字体的向量被获取并选择合适的缩放因素来适应字体大小。
这个字体管理器的问题是,在创建表单时,当代码将字体向量放进哈希表时,却没有定义表单删除时对向量的移除。因此,这个在整个应用程序的生命周期都存在的静态的哈希表,却从来没有移除指向每个表单的键值。所以,所有的表单和其相关联的类被遗留在了内存中。
问题修正
对于这个问题的简单解决方案就是字体管理器增加一个方法,来允许哈希表的 remove() 方法会在用户删除表单时被调用到。增加的 removeKeyFromHashtables() 方法如下所示:
[java]
public void removeKeyFromHashtables(GraphCanvas graph) {
if (graph != null) {
viewFontTable.remove(graph); // remove key from hashtable
// to prevent memory leak
}
}

然后,我在 FormFrame 类里添加了对这个方法的一个调用。FormFrame 使用 Swing 的内部框架来实现表单 UI,因此对于字体管理器的调用被添加到当内部框架完全关闭时所执行的方法,如下所示:
[java]
/**
* Invoked when a FormFrame is disposed. Clean out references to prevent
* memory leaks.
*/
public void internalFrameClosed(InternalFrameEvent e) {
FontManager.get().removeKeyFromHashtables(canvas);
canvas = null;
setDesktopIcon(null);
}

在我对代码做出修改以后,我使用调试工具来确认在相同的测试用例被执行时删除表单所关联到的对象的数目。
内存泄漏的防止
可以通过对一些常见问题的注意来防止内存泄漏。容器类,比如哈希表和向量,是找到引起内存泄漏的常见的地方。尤其是当这些类被声明为静态的并存活于应用程序的整个生命周期之中时。
另一个常见(导致内存泄漏的)问题是当你将一个类注册为事件监听器,却没考虑到当这个类不再需要时将其注销。还有,指向其他类的成员变量在恰当的时候要设置为 null。
结束语
寻找内存泄漏的原因可能是一个繁琐的过程,还没有提到的一点是这将需要特殊的调试工具。然而,一旦你熟悉了追踪对象引用的工具和模式,你将能够跟踪内存泄漏。此外,你还会获得一些有价值的技能,不仅可以节省项目编程投入,而且在以后的项目中你将拥有找出可以防止发生内存泄漏的编程做法的眼光。
相关资料
Quest Software 的 JProbe
Borland 的 Optimizeit Enterprise
Paul Moeller 的 Win32 Java Heap Inspector