Java程序里的内存泄漏是如何表现的

  • 格式:docx
  • 大小:26.71 KB
  • 文档页数:5

下载文档原格式

  / 5
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Java程序里的内存泄漏是如何表现的

大多数程序员都知道使用类似于Java 的编程语言的好处之一就是他们无需再为内存的分配和释放所担心了。你只需要简单地创建对象,当它们不再为程序所需要时Java 会自行通过一个被称为垃圾收集的机制将其移除。这个过程意味着Java 已经解决了困扰其他编程语言的一个棘手的问题-- 可怕的内存泄漏。果真是这样的吗?

在进行深入讨论之前,让我们先回顾一下垃圾收集是如何进行实际工作的。垃圾收集器的工作就是找到程序不再需要的对象并在当它们不再被访问或引用时将它们移除掉。垃圾收集器从贯穿整个程序生命周期的类这个根节点开始,扫描所有引用到的节点。在遍历节点时,它跟踪那些被活跃引用着的对象。那些不再被引用的对象就满足了垃圾回收的条件。当这些对象被移除时被它们占用的内存资源会交还给Java 虚拟机(JVM)。

因此Java 代码的确不需要程序员负责内存管理的清理工作,它自行对不再使用的对象进行垃圾收集。然而,需要记住的是,垃圾收集的关键在于一个对象在不再被引用时才被统计为不再使用。下图对这一概念进行了说明。

上图表示在一个Java 程序执行时具有不同的生命周期的两个类。类A 首先被实例化,它存在的时间比较长,几乎贯穿整个进程的生命周期。在某个时间点,类B 被创建,类A 添加了一个对这个新建类的引用。我们假设类B 是某个用于显示并返回用户指令的用户界面部件。尽管类B 不再被使用,如果类A 对类B 的引用未被清除,类B 将继续存在并占据内存空间,即使下一次垃圾收集被执行。

什么时候需要注意内存泄漏?

如果在你的程序执行一段时间之后遇到ng.OutOfMemoryError 的话,内存泄漏无疑是最值得怀疑的。除了这种明显的情况之外,什么时候需要考虑内存泄漏?完美主义的程序员会回答说所有的内存泄漏都需要进行审查和更改。然而,在跳到这一结论之前还需要考虑其他几点因素,包括程序的生命周期以及内存泄漏的大小。

考虑一下在一个程序的生命周期里垃圾收集器可能从未执行的情况。无法保证什么时候JVM 会调用垃圾收集-- 即使程序显式调用System.gc()。通常情况下,垃圾收集器不会自动运行,直到程序需要比目前可用内存还要多的内存。此时,JVM 会首先尝试调用垃圾收集器以获取更多可用内存。如果这个尝试仍旧不能够释放出足够的资源,JVM 将会从操作系统获取更多内存,直到达到所允许内存的最大值。

举个例子来说,一个小型的Java 应用程序,用来显示一些简单的配置修改的用户界面元素,出现了内存泄漏。垃圾收集器可能在程序关闭之前都不会被调用到,因为JVM 可能总是有足够的内存来创建程序所需要的所有对象。因此,在这种情况下,即便是一些已死对象在程序运行的时候仍旧占据着内存,但这并不影响实际应用。

如果开发中的Java 代码将以每天24 小时运行在服务器上,这时内存泄漏将会比上面的那个配置工具程序要明显的多了。即便是代码中最小的内存泄漏,在持续运行的情况下最终也将耗尽所有可用内存。

相反的情况下,即使一个程序只是短暂存活,却分配了大量临时对象(或者少量的占用大量内存的对象),在这些对象不再需要时没有取消引用,这样的Java 代码也会达到内存限制。

最后一个值得注意的问题是,不必过于担心(Java 程序所造成的)内存泄漏。Java 内存泄漏不应该被认为是像其他语言中所发生的那样危险,比如C++ 的内存丢失将永远不会返回给操作系统。Java 应用程序中,我们把不再需要的却占据着内存资源的对象都交给JVM.所以在理论上来说,一旦Java 程序和它的JVM 关闭掉,所有分配的内存都将归还给操作系统。

如何断定程序具有内存泄漏

查看一个运行在Windows NT 平台上的Java 程序是否具有内存泄漏,你可以简单地在程序运行的时候去观察任务管理器中的内存设置。然而,在观察一些运行中的Java 程序之后,你会发现,它们跟本地应用程序相比使用更多内存。我开发过的一些Java 项目会启用10 到20 MB 的系统内存。与这个数字相比,本地的操作系统自带的Windows Explorer 程序使用到5 MB.

另外一个关于Java 程序的内存使用要注意的是典型的运行在IBM JDK1.1.8 JVM 上的程序似乎在

其运行时不断吞噬了越来越多的系统内存。程序似乎永远不会返回一些内存给操作系统,直到一个非常大的物理内存分配给它。这会不会就是内存泄漏的迹象?

要明白是怎么回事,我们需要熟悉JVM 是如何将系统内存使用作自己的堆的。在运行java.exe 时,你可以使用一些特定的选项来控制垃圾收集的堆的启动容量和最大容量(分别是-ms 和-mx)。Sun 的JDK 1.1.8 默认使用1 MB 的启动设置和16 MB 的最大设置。IBM JDK 1.1.8 默认使用机器物理内存容量的一半作为最大设置。这些内存设置对JVM 发生内存溢出时的做法具有直接影响,这时JVM 可能会继续增长堆内存,而不是等待一个垃圾回收的结束。

因此为了寻找并最终消除内存泄漏,我们需要比任务监视程序更好的工具。当你想检测内存泄漏的时候内存调试程序(参见下文的参考资料)可以派上用场了。这些程序通常会给你关于堆内存里对象的数量、

每个对象实例的个数以及对象使用中的内存等一些信息。此外,它们还会提供很有用的视图,这些视图可以显示每个对象的引用和引用者,以便你跟踪内存漏洞的来源。

接下来,我将展示如何使用Sitraka Software 的JProbe 调试工具来检测和消除内存泄漏,希望会对你就如何部署这些工具并成功消除内存泄漏产生一些启发。

一个内存泄漏的例子

这个示例主要展示了我们部门开发的一个商业版应用的一个问题,这个问题在JDK 1.1.8 上工作了几个小时后被测试人员找出来。这个Java 应用程序的相关代码和包是由几个不同团队的程序员开发出来的。程序里出现的内存泄漏的原因,我怀疑,是由一些没有真正理解其他(团队)开发的代码的程序员所引起。讨论中的Java 代码允许用户不必去写Palm OS 本地代码来创建Palm 个人数码助理应用。通过使用图形界面,用户可以创建表单,使用控件对它们进行填充,然后连接控件事件来创建Palm 应用程序。测试人员发现,这个Java 应用最终发生了内存溢出--表单和控件的创建和删除延时。开发人员并没有发现这个问题存在,因为他们的机器(相对Palm)拥有着更多的物理内存。

为了讨论这个问题,我使用了JProbe 来断定问题的存在。即使拥有JProde 提供的强大工具和内存快照,调查仍然是一个繁琐的、反复的过程,它涉及先确定内存泄漏的原因,然后做出代码更改并验证其效果。

JProbe 有几个选项来控制在一次调试回话期间什么样的信息会被记录。经过一些试验后,我判定获取所需信息的最有效的方式是关掉性能数据收集,专注于捕获的堆数据。JProbe 提供了一个叫做运行时堆摘要的视图来显示Java 应用程序在一段时间内使用的堆内存的数量。它同时也提供了一个工具栏按钮用来在需要时强制JVM 执行垃圾收集--在想要看一下一个类的给定实例不再为Java 应用程序需要时是否会被垃圾收集,这个功能是很有用的。下图显示了在一段时间内使用的堆存储量。

在堆使用情况图中,蓝色部分表示已分配的堆空间量。我启动Java 程序之后它达到了一个稳定点,我强制垃圾收集器执行,这由绿线之前的蓝色曲线的一个骤降表示(这条绿线表示一个检查点被插入)。接下来,我先是添加而后删掉了四个表单并再次调用垃圾收集器。检查点之后的蓝色曲线的水平线比检查点之前的蓝色曲线的水平线高的事实告诉我们很可能出现了内存泄漏,因为该程序已经回归其只有一个简单可见的表单的初始状态。我检查实例确认了泄漏。总之,结果表明FormFrame 类(表单的主UI 类)的数量在检查点之后增加了四个。

寻找原因

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

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

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

对于这个具体的例子,找到的罪魁祸首是一个包含一个静态的哈希表的字体管理类。通过引用列表追踪后,我发现根节点是一个静态的哈希表,这个哈希表保存了每个表单使用的字体。各种表单可以被独立