现在的位置: 首页 > 综合 > 正文

处理 Java 程序中的内存漏洞(下)

2018年04月27日 ⁄ 综合 ⁄ 共 3518字 ⁄ 字号 评论关闭
处理 Java 程序中的内存漏洞(下)
研究何时应该关注内存漏洞以及如何预防内存漏洞
(作者:转自IBM DeveloperWorks Jim Patrick 2001年04月12日 11:40)

  下面我将说明我是如何用 Sitraka Software 的 JProbedebugger 检测和去除内存漏洞的,以使您对这些工具的部署方式以及成功去除漏洞所需的过程有所了解。

内存漏洞的一个示例
  本例集中讨论一个问题,我们部门当时正在开发一个商业发行版软件,这是一个 Java JDK 1.1.8 应用程序,一个测试人员花了几个小时研究这个程序才最终使这个问题显现出来。这个 Java 应用程序的基本代码和包是由几个不同的开发小组在不同的时间开发的。我猜想,该应用程序中意外出现的内存漏洞是由那些没有真正理解别人开发的代码的程序员造成的。

  我们正在讨论的 Java 代码允许用户为 Palm 个人数字助理创建应用程序,而不必编写任何 Palm OS 本地代码。通过使用图形用户界面,用户可以创建窗体,向窗体中添加控件,然后连接这些控件的事件来创建 Palm 应用程序。测试人员发现,随着不断创建和删除窗体和控件,这个 Java 应用程序最终会耗尽内存。开发人员没有检测到这个问题,因为他们的机器有更多的物理内存。

  为了研究这个问题,我用 JProbe 来确定什么地方出了差错。尽管用了 JProbe 所提供的强大工具和内存快照,研究仍然是一个冗长乏味、不断重复的过程,首先要确定出现内存漏洞的原因,然后修改代码,最后还得检验结果。

  JProbe 提供几个选项,用来控制调试期间实际记录哪些信息。经过几次试验以后,我断定获取所需信息的最有效方法是,关闭性能数据收集,而将注意力集中在所捕获的堆数据上。JProbe 提供了一个称为 Runtime Heap Summary 的视图,它显示 Java 应用程序运行时所占用的堆内存量随时间的变化。它还提供了一个工具栏按钮,必要时可以强制 JVM 执行垃圾收集。如果您试图弄清楚,当 Java 应用程序不再需要给定的类实例时,这个实例会不会被作为垃圾收集,这个功能将很有用。图 2 显示了使用中的堆存储量随时间的变化。


  图 2. Runtime Heap Summary

  在 Heap Usage Chart 中,蓝色部分表明已分配的堆空间大小。在启动这个 Java 程序并达到稳定状态以后,我强制垃圾收集器运行,在图中的表现就是绿线(这条线表明插入了一个检查点)左侧的蓝线的骤降。随后,我添加了四个窗体,然后又将它们删除,并再次调用了垃圾收集器。当程序返回仅有一个可视窗体的初始状态时,检查点之后的蓝色区域高于检查点之前的蓝色区域这一情况表明可能存在内存漏洞。我通过查看 Instance Summary 证实确实有一个漏洞,因为 Instance Summary 表明 FormFrame 类(它是窗体的主用户界面类)的计数在检查点之后增加了 4。

查找原因
  为了将测试人员报告的问题剔出,我采取的第一个步骤是找出几个简单的、可重复的测试案例。就本例而言,我发现只须添加一个窗体,将它删除,然后强制执行垃圾收集,结果就会导致与被删除窗体相关联的许多类实例仍然处于活动状态。这个问题在 JProbe 的 Instance Summary 视图中很明显,这个视图统计每个 Java 类在堆中的实例数。

  为了查明使垃圾收集器无法正常完成其工作的那些引用,我使用 JProbe 的 Reference Graph(如图 3 所示)来确定哪些类仍然引用着目前未被删除的 FormFrame 类。在调试这个问题时该过程是最复杂的过程之一,因为我发现许多不同的对象仍然引用着这个无用的对象。用来查明究竟是哪个引用者真正造成这个问题的试错过程相当耗时。

  在本例中,一个根类(左上角用红色标明的那个类)是问题的发源地。右侧用蓝色突出显示的类处在从最初的 FormFrame 类跟踪而来的路径上。


  图 3. 在引用图中跟踪内存漏洞

  就本例而言,最后查明罪魁祸首是包含一个静态 hashtable 的字体管理器类。通过逆向追踪引用者列表,我发现根节点是用来存储每个窗体所用字体的一个静态 hashtable。各个窗体可被单独放大或缩小,所以这个 hashtable 包含一个具有某个给定窗体的全部字体的 vector。当窗体的大小改变时,就会提取这个字体 vector,并将适当的缩放因子应用于字体大小。

  这个字体管理器类的问题是,虽然程序在创建窗体时将字体 vector 存入这个 hashtable 中,但没有提供在删除窗体时删除 vector 的代码。因此,这个静态 hashtable(在应用程序的生存期内一直存在)永远不会删除引用每个窗体的那些键。结果,窗体及其所有关联的类都闲置在内存中。

修正
  本问题的一个简单解决方案是在字体管理器类中添加一个方法,以便在用户删除窗体时以适当的键作为参数调用 hashtable 的 remove() 方法。removeKeyFromHashtables() 方法如下所示:

  public void removeKeyFromHashtables(GraphCanvas graph) {

   if (graph != null) {

    viewFontTable.remove(graph);   // 删除 hashtable 中的键

                     // 以预防内存漏洞

   }

  }

  随后,我在 FormFrame 类中添加了一个对此方法的调用。FormFrame 实际上是使用 Swing 的内部框架来实现窗体用户界面的,所以我将对字体管理器的调用添加到当完全关闭内部框架时所调用的方法中,如下所示:

  /**

  * 当去掉 (dispose) FormFrame 时调用。清除引用以预防内存漏洞。

  */

  public void internalFrameClosed(InternalFrameEvent e) {

   FontManager.get().removeKeyFromHashtables(canvas);

   canvas = null;

   setDesktopIcon(null);

  }

  当作了这些修改以后,我使用调试器证实:当执行相同的测试案例时,与被删除的窗体相关联的对象计数减小。

预防内存漏洞
  可以通过观察某些常见问题来预防内存漏洞。Collection 类(如 hashtable 和 vector)常常是出现内存漏洞的地方。当这个类被用 static 关键字声明并且在应用程序的整个生存期中存在时尤其是这样。

  另一个常见的问题是,您将一个类注册为事件监听程序,而在不再需要这个类时没有撤销注册。此外,您常常需要在适当的时候将指向其他类的类成员变量设置为 null。

小结
  查找内存漏洞的原因可能是一个乏味的过程,更不用说需要专用调试工具的情况了。但是,一旦您熟悉了这些工具以及在跟踪对象引用时进行搜索的模式,您就能够找到内存漏洞。此外,您还会摸索出一些有价值的技巧,这些技巧不仅有助于节约项目的成本,而且使您能够领悟到在以后的项目中应该避免哪些编码方式来预防内存漏洞。

参考资源
  在开始查找内存漏洞之前,请先熟悉下列一种调试器:

●Sitraka Software 的 JProbe Profiler withMemory Debugger
●Intuitive System 的
Optimizeit Java Performance Profiler
●Paul Moeller 的
Win32Java Heap Inspector
●IBM alphaWorks 网站上的
Jinsight
Jinsight:
A tool for visualizing the execution of Java programs(developerWorks,1999 年 11 月)详细说明了这个实用程序是如何帮助您分析性能和调试代码的。
注:本文讨论的项目是用 JDK 1.1.8 完成的,但 JDK 1.2 引入了一个新包,
java.lang.ref,这个包可与垃圾收集器交互。另外,JDK 1.2 还引入了一个 java.util.WeakHashMap 类,可用它来代替传统的 java.util.Hashtable 类。这个类不会阻止垃圾收集器回收键对象。JDK 1.3 的 Solaris、Linux 和 Microsoft Windows 版本引入了 Java HotSpot Client VM,该虚拟机带有一个新的、经过改进的垃圾收集器。
作者简介
Jim Patrick 是 IBM Pervasive Computing Division 的一名顾问程序员。他从 1996 年开始用 Java 编写程序。请通过
patrickj@us.ibm.com 与 Jim 联系。
全文完|
上一页

抱歉!评论已关闭.