2001年就有的Java内存泄漏,你还在踩坑?程序员别再装睡了

📅 2026/7/21 6:42:19 👁️ 阅读次数 📝 编程学习
2001年就有的Java内存泄漏,你还在踩坑?程序员别再装睡了

译序: 关于Java的内存泄漏, 它可不是个新鲜话题。Jim写的这篇文章, 早在2001年就已问世。然而, 其绝非表明Java的内存泄漏是个过时的、甚至于不重要的话题。恰恰相反, 关乎Java的内存泄漏, 理应成为每一位在意程序健壮性、高性能的程序员务必去了解的知识。

本文将揭示什么时候需要关注内存泄漏以及如何进行防止。

摘要: Java程序当中居然也会存在内存泄漏是吗? 没错。和普遍流行的看法截然不同, 内存管理在Java编程之际依旧是需要去考虑的事项。在这一篇文章之内 , 你能够清楚知道是哪些缘由致使了Java内存泄漏, 以及在何时应当对这些泄漏予以关注。你同样将会学习内容简略同时却实用的一门课程, 以此来应对自身项目里的内存泄漏。

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

多数程序员清楚, 采用诸如Java这般的编程语言, 好处之一便是, 无需再忧心内存的分配与释放事宜。只需简单创建对象, 当其不再被程序所需时, Java会经由一种名为垃圾收集的机制, 自行将其移除。此过程表明, Java已解决困扰其他编程语言的一个棘手难题, 即可怕的内存泄漏。果真如此吗?

在着手展开深入探讨之前, 咱们先来回顾一番垃圾收集到底是怎样开展实际工作的, 垃圾收集器的职责在于寻觅程序不再必需的对象, 并且在它们不再被访问或者引用之际把它们给移除掉, 垃圾收集器从贯穿程序整个生命周期的类这个根起始点出发查看, 扫描所有被引用到的节点, 在遍历节点的过程中, 它追踪那些处于活跃引用状态的对象, 那些不再被引用的对象便符合了垃圾回收的条件, 当这些对象被移除时分, 被它们占据而使用的内存资源会归还给 Java 虚拟机(JVM)。

因此, Java代码确实无需程序员去负责内存管理的清理事宜,它会自行针对不再使用的对象展开垃圾收集。然而, 需要牢记的是, 垃圾收集的关键之处在于, 唯有当一个对象不再被引用的时候, 才会被算作不再使用。下图针对这一概念作出了说明。

如下图示, 在一个Java程序运行之际, 有两个具备不同生命周期的类存在。类A率先被实例化, 其存在时长较为漫长, 近乎从头至尾历经整个进程的生命周期。于某一时刻, 类B被创建出来,类A增添了一个针对这个新创建之类的引用。我们所假定的, 类B乃是某个用于呈现并返回用户指令的用户界面部件。即便类B不再被加以使用, 要是类A对类B的引用未被清除掉, 类B将会持续存在且占据着内存空间, 哪怕下一次垃圾收集得以执行。

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

若是在你所编写的程序运行了一段时间之后, 碰到了java.lang.这种情况, 那么内存泄漏无疑是最值得被怀疑的对象。除开这种十分明显的情形之外, 究竟在什么时候是需要去考虑内存泄漏这一问题的呢? 有着完美主义倾向的程序员会给出这样的回答, 即所有的内存泄漏都是需要展开审查以及进行更改操作的。然而, 在直接跳到这一结论之前, 其实还需要去考量其他的几点因素, 其中涵盖了程序的生命周期以及内存泄漏的规模大小。

想一想一个程序的整个生命周期之中, 存在垃圾收集器可能始终都不执行的情形。没办法确保JVM究竟会在何时去调用垃圾收集, 哪怕程序明确地调用.gc() 的时候。正常状况下, 垃圾收集器不会自动运行, 一直到程序需要的内存比当前可供使用的内存更多的时候。这时候, JVM会率先尝试去调用垃圾收集器, 以便得到更多可供使用的内存。要是此尝试依旧无法释放出足够的资源, JVM便会从操作系统那里获取更多内存, 直至达到所允许内存的最大上限时为止。

比如, 有一个小型的Java应用程序, 能用以显示一些含有简单配置修改的用户界面元素, 它出现了内存泄漏。对于此, 垃圾收集器有可能一直都在系统关闭之前都不会被调用到, 这是因为JVM或许一直是总能有充足的可以让其用来创建程序所需要的全部对象的内存。所以, 处于这种状况下, 就算是存在一些已经死亡的对象在程序运行之际依旧是占据着内存, 然而这是不会对实际应用产生影响的。

假设正在开发的Java代码会在服务器上保持每日24小时不间断运行, 那么此时内存泄漏的情况相较于上面提到的那个配置工具程序而言, 将会显著得多。哪怕是代码里极其微小的内存泄漏, 在持续运行的状况下, 最终也势必会将全部可用内存消耗殆尽。

与之相反的情形下,哪怕一个程序仅仅是短暂地存在, 然而在运行期间却分配了好多临时对象, 或者是分配了少量的但占用了大量内存空间的对象, 当这些对象不再被需要的时候却没有进行取消引用的操作, 如此这般的Java代码同样会达到内存的限制。

这里有个最后得留意的问题, 那就是别太忧心(Java程序引发的)内存泄漏, Java内存泄漏可不应当被视作像其他语言里出现的那般危险, 就像C++的内存丢失永远不会返还给操作系统那样在Java应用程序当中, 对那些不再需要却霸占着内存资源的对象, 我们都交给JVM去处理只要Java程序跟它的JVM关闭了理论上分配的所有内存都会归还给操作系统。

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

查看一个处在 NT 平台上运行的 Java 程序是不是存在内存泄漏, 你能够简便地于程序运行时去留意任务管理器里的内存相关设置, 然而, 在留意了一些正在运行的 Java 程序后, 你会发觉, 它们跟本地应用程序相较使用了更多内存, 我所开发的一些 Java 项目会开启 10 至 20 MB 的系统内存, 与此数字相比, 本地操作系统自带的程序用到了 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 有可能会继续让堆内存增长, 而并非等待一次垃圾回收的完结。

所以, 为了寻觅并最终将内存泄漏予以消除, 我们所需的是比任务监视程序更为优良的工具。当你打算去检测内存泄漏情况时, 内存调试程序(可参见下文的参考资料)便能够发挥作用了。这些程序一般会给予你有关堆内存当中对象的数量、每个对象实例的数目以及对象使用过程中的内存等方面的一些信息。除此之外, 它们还会提供颇为有用的视图, 这些视图能够显示每个对象的引用以及引用者, 从而便于你去追踪内存漏洞的源头。

接着, 我会呈现怎样运用那个调试工具去检测以及消除内存泄漏, 期望能对你在怎样部署这种工具并成功消除内存泄漏这件事上生出些许启发性的作用。

一个内存泄漏的例子

此示例着重呈现了, 我们部门所开发的商业版应用存在的一个问题, 该问题经测试人员在 JDK 1.1.8 上运行数小时后被找出。此 Java 应用程序相关的代码以及包, 是由几个不同团队的程序员予以开发的。对于程序中出现内存泄漏的缘由, 我存有怀疑, 觉得是由一些未能真正领会其他(团队)所开发代码的程序员导致的。讨论中的 Java 代码, 可让用户无需编写 Palm OS 本地代码去创建 Palm 个人数码助理应用。利用图形界面, 用户能够創建表单, 借助控件对其予以填充, 接着连接控件事件去创建 Palm 应用程序。测试人员察觉到, 这个 Java 应用最终出现了内存溢出情况——表单以及控件的创建与删除存在延时现象。开发人员并未发觉此问题的存在, 鉴于他们的机器(相较于 Palm)具备更多的物理内存。

为了探讨这个问题, 我借助 来判定问题的存在。即便具备 所供给的强大工具以及内存快照, 调查依旧是个繁杂的、反复的进程, 该进程包括先找准内存泄漏的缘由, 接着进行代码更改并证实其成效。

对在一次调试回话期间, 什么样啥样的信息将会被记录, 存在着好几个用作控制的选项给着来, 试过后发现它这情况。在进行了那么点试验之后, 我做出了判断这一判定, 那就是获取所需那所需滴信息的最为有效的方式为哪种方式, 是把性能数据收集给关掉, 去把注意力侧重于专注于捕获来的堆数据之上!给提供了存在着称作运行时堆摘要的东西一个视图, 这个视图它用来做啥, 用来显示 Java 应用程序在一段时期时间内所使用的堆内存的数量情况的!它还提供了这样一个工具栏按钮, 那就是在有需求之际用来强行让 JVM 去执行垃圾收集, 在期望弄清楚一个类的特定某个实例, 在不再被 Java 应用程序所需之时, 会不会遭到垃圾收集的情况下, 这个功能相当有用。下面这张图展示了一段时期之内那个正被所使用着的堆存储数量。

在堆使用情况图里, 蓝色部分展现的是已分配的堆空间量, 我启动Java程序后它进入了个稳定状态, 我强制垃圾收集器运行, 这由绿线之前蓝色曲线的一次骤降体现, 这条绿线意味着一个检查点被插入, 接下来, 我先添加了四个表单随后又删除了它们, 再次调用垃圾收集器, 检查点之后蓝色曲线的水平线比检查点之前蓝色曲线的水平线高这一情况表明很可能发生了内存泄漏, 因为该程序已回到其仅有一个简单可见表单的初始状态。我通过对实例展开检查从而确认了存在泄漏的情况。总而言之, 这般结果显示出, 类, 也就是表单的主UI类, 其数量在检查点过后增添了四个。

寻找原因

要想把测试人员所提交的问题给隔离出来, 第一步便是提供一些简单的、会重复的测试用例。以上面那一个例子作为例子, 我发觉简单地去添加一个表单、删除这个表单, 接着强制垃圾收集器, 其结果是一些关联到已经被删除掉的表单的实例依旧存活着。这种问题借助实例摘要视图来看是明显可见的, 该视图统计了堆内存里每个类的实例的个数。

要在垃圾收集器工作时, 定位具体实例的引用, 我借助了显示引用的画面, 如以下图示, 用以判定仍是哪些类在引用已被清除的类。这是调试此类问题的有效途经, 借此我发现诸多不同对象仍在引用那些无用对象。然而, 通过反复尝试来确定究竟是哪个引用者引发该问题的过程耗费时间。

在这起案例之内, 根类(位于左上角呈现红色的那个)乃是出现问题的起始源头。右侧以蓝色进行突出显示的那个, 此类便是最终追踪寻访到的。

有一个字体管理类, 它里边包含一个静态的哈希表, 对于这个具体例子而言, 找到的罪魁祸首就是它。去追踪引用列表得到相关情况, 之后发现根节点是一个静态的哈希表, 这个哈希表存着每个表单采用的字体。种种表单能够独立地进行放大或者缩小, 所以那个哈希表含有一个这样的向量, 此向量有着每个指定表单的所有字体。当表单的缩放视图变动的时候, 那个带有字体的向量会被获取, 并且要挑选合适的缩放因素, 借助这些因素来契合字体大小。

这个字体管理器存在问题, 在创建表单之际, 代码把字体向量放入哈希表时, 未对表单删除时向量的移除做出定义。于是, 有这样一个贯穿整个应用程序生命周期的静态哈希表, 其从来不曾移除指向每个表单的键值。所以, 所有表单及其关联类都遗留在内存里了。

问题修正

围绕这个问题, 有个简易的解决办法, 是在字体管理器里增添一种方法, 可以让哈希表的括号内方法当用户把表单删除之际能被调用到。增添之时, 还有一个les方法是下面如此呈现呢:

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

接着, 我于类之中增添了针对这个方法的一次调用。借助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。

结束语

探寻内存泄漏的缘由兴许是个繁杂的进程, 尚未提及的一点是这会需用特殊的调试工具。但, 一旦你熟知追溯对象引用的工具以及模式, 就能够追踪内存泄漏。另外, 你会获取一些具价值的技能, 这既能节省项目编程投入, 并且在往后的项目里你会具备找出既能预防内存泄漏发生的编程举措的眼光。