寻找原因

  要想将测试人员提交的问题隔离出来,第一步是提供一些简单的、重复的测试用例。以上面那个例子为例,我发现简单地添加一个表单,删除这个表单,然后强制垃圾收集器的结果是一些关联到已经删除掉的表单的实例仍然存活着。这种问题通过 JProbe实例摘要视图来看是显而易见的,视图中统计了堆内存中每个类的实例的个数。

  要定位垃圾收集器工作时具体实例的引用,我使用了 JProbe 的引用画面,如下图所示,来断定哪些类仍然在引用已被删除掉的 FormFrame 类。这是调试这种问题的巧妙地方法之一,我通过它发现了很多不同的对象仍然在引用那些无用的对象。而通过试错来查明究竟是哪个引用者真正造成这个问题的过程却是相当耗时的。

  在这个案例中,根类(左上角红色的那个)是出现问题的起源。右侧用蓝色突出的那个类是追踪到的 FormFrame 类。

 

  对于这个具体的例子,找到的罪魁祸首是一个包含一个静态的哈希表的字体管理类。通过引用列表追踪后,我发现根节点是一个静态的哈希表,这个哈希表保存了每个表单使用的字体。各种表单可以被独立地放大或缩小,所以哈希表包含了一个具有每个指定的表单的所有字体的向量。当表单的缩放视图改变时,带有字体的向量被获取并选择合适的缩放因素来适应字体大小。

  这个字体管理器的问题是,在创建表单时,当代码将字体向量放进哈希表时,却没有定义表单删除时对向量的移除。因此,这个在整个应用程序的生命周期都存在的静态的哈希表,却从来没有移除指向每个表单的键值。所以,所有的表单和其相关联的类被遗留在了内存中。

  问题修正

  对于这个问题的简单解决方案是字体管理器增加一个方法,来允许哈希表的 remove() 方法会在用户删除表单时被调用到。增加的 removeKeyFromHashtables() 方法如下所示:

public void removeKeyFromHashtables(GraphCanvas graph) {
  if (graph != null) {
    viewFontTable.remove(graph);     // remove key from hashtable
                                     // to prevent memory leak
  }
}


  然后,我在 FormFrame 类里添加了对这个方法的一个调用。FormFrame 使用 Swing 的内部框架来实现表单 UI,因此对于字体管理器的调用被添加到当内部框架完全关闭时所执行的方法,如下所示:

/**
* 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。

  结束语

  寻找内存泄漏的原因可能是一个繁琐的过程,还没有提到的一点是这将需要特殊的调试工具。然而,一旦你熟悉了追踪对象引用的工具和模式,你将能够跟踪内存泄漏。此外,你还会获得一些有价值的技能,不仅可以节省项目编程投入,而且在以后的项目中你将拥有找出可以防止发生内存泄漏的编程做法的眼光。