译序:Java 的内存泄漏,这不是一个新话题。Jim Patrick 的这篇文章早在 2001 年就写出来了。但这并不意味着 Java 的内存泄漏是一个过时了的甚至不重要的话题。相反,Java 的内存泄漏应当是每一个关心程序健壮性、稳定性和高性能的程序员所必须了解的知识。
本文将揭示什么时候需要关注内存泄漏以及如何进行防止。
摘要:Java 程序里也存在内存泄漏?当然。和流行的看法相反,内存管理仍然是 Java 编程时应该考虑的事情。在这篇文章里,你会了解到是什么原因导致了 Java 内存泄漏以及什么时候需要对这些泄漏进行关注。你也将会学到一个快速实用的课程以应对自己项目中的内存泄漏。
Java 程序里的内存泄漏是如何表现的
大多数程序员都知道使用类似于 Java 的编程语言的好处之一就是他们无需再为内存的分配和释放所担心了。你只需要简单地创建对象,当它们不再为程序所需要时 Java 会自行通过一个被称为垃圾收集的机制将其移除。这个过程意味着 Java 已经解决了困扰其他编程语言的一个棘手的问题 -- 可怕的内存泄漏。果真是这样的吗?
在进行深入讨论之前,让我们先回顾一下垃圾收集是如何进行实际工作的。垃圾收集器的工作就是找到程序不再需要的对象并在当它们不再被访问或引用时将它们移除掉。垃圾收集器从贯穿整个程序生命周期的类这个根节点开始,扫描所有引用到的节点。在遍历节点时,它跟踪那些被活跃引用着的对象。那些不再被引用的对象就满足了垃圾回收的条件。当这些对象被移除时被它们占用的内存资源会交还给 Java 虚拟机(JVM)。
因此 Java 代码的确不需要程序员负责内存管理的清理工作,它自行对不再使用的对象进行垃圾收集。然而,需要记住的是,垃圾收集的关键在于一个对象在不再被引用时才被统计为不再使用。下图对这一概念进行了说明。
上图表示在一个 Java 程序执行时具有不同的生命周期的两个类。类 A 首先被实例化,它存在的时间比较长,几乎贯穿整个进程的生命周期。在某个时间点,类 B 被创建,类 A 添加了一个对这个新建类的引用。我们假设类 B 是某个用于显示并返回用户指令的用户界面部件。尽管类 B 不再被使用,如果类 A 对类 B 的引用未被清除,类 B 将继续存在并占据内存空间,即使下一次垃圾收集被执行。
什么时候需要注意内存泄漏?
如果在你的程序执行一段时间之后遇到 java.lang.OutOfMemoryError 的话,内存泄漏无疑是最值得怀疑的。除了这种明显的情况之外,什么时候需要考虑内存泄漏?完美主义的程序员会回答说所有的内存泄漏都需要进行审查和更改。然而,在跳到这一结论之前还需要考虑其他几点因素,包括程序的生命周期以及内存泄漏的大小。
考虑一下在一个程序的生命周期里垃圾收集器可能从未执行的情况。无法保证什么时候 JVM 会调用垃圾收集 -- 即使程序显式调用 System.gc()。通常情况下,垃圾收集器不会自动运行,直到程序需要比目前可用内存还要多的内存。此时,JVM 会首先尝试调用垃圾收集器以获取更多可用内存。如果这个尝试仍旧不能够释放出足够的资源,JVM 将会从操作系统获取更多内存,直到达到所允许内存的最大值。
举个例子来说,一个小型的 Java 应用程序,用来显示一些简单的配置修改的用户界面元素,出现了内存泄漏。垃圾收集器可能在程序关闭之前都不会被调用到,因为 JVM 可能总是有足够的内存来创建程序所需要的所有对象。因此,在这种情况下,即便是一些已死对象在程序运行的时候仍旧占据着内存,但这并不影响实际应用。
如果开发中的 Java 代码将以每天 24 小时运行在服务器上,这时内存泄漏将会比上面的那个配置工具程序要明显的多了。即便是代码中最小的内存泄漏,在持续运行的情况下最终也将耗尽所有可用内存。
相反的情况下,即使一个程序只是短暂存活,却分配了大量临时对象(或者少量的占用大量内存的对象),在这些对象不再需要时没有取消引用,这样的 Java 代码也会达到内存限制。
最后一个值得注意的问题是,不必过于担心(Java 程序所造成的)内存泄漏。Java 内存泄漏不应该被认为是像其他语言中所发生的那样危险,比如 C++ 的内存丢失将永远不会返回给操作系统。Java 应用程序中,我们把不再需要的却占据着内存资源的对象都交给 JVM。所以在理论上来说,一旦 Java 程序和它的 JVM 关闭掉,所有分配的内存都将归还给操作系统。