1. 简介

JDK1.2其中一点优势就是提供内存管理,从而使开发人员免受显式内存管理的复杂性的影响。

本文概述了Sun J2SE 5.0版本中Java HotSpot虚拟机(JVM)中的内存管理。 它描述了可用于执行内存管理的垃圾收集器,并提供了有关选择和配置收集器以及为收集器运行所在的内存区域设置大小的一些建议。 它还提供了一些资源,列出了一些影响垃圾收集器行为的最常用选项,并提供了许多更详细文档的链接。

第2节适用于对自动内存管理概念不熟悉的读者。 它简要讨论了这种管理的好处,而不是要求程序员为数据显式分配空间。 然后,第3节概述了一般垃圾收集概念,设计选择和性能指标。 它还根据对象的预期生存时间将常用的内存组织引入到称为分代的不同区域。 事实证明,这种分代的方法有效地减少了垃圾回收暂停时间,并减少了各种应用程序的总体成本。

本文的其余部分提供了特定于HotSpot JVM的信息。 第4节描述了四个可用的垃圾收集器,其中一个是J2SE 5.0 update 6中的新垃圾收集器,并记录了它们全部利用的分代内存组织。 对于每个收集器,第4节总结了所使用的收集算法的类型,并指定了何时选择该收集器。

第5节介绍了J2SE 5.0发行版中的一项新技术,该技术结合了(1)基于运行应用程序的平台和操作系统自动选择垃圾收集器,堆大小和HotSpot JVM(客户端或服务器),以及(2)根据用户指定的期望行为进行动态垃圾收集优化。 该技术称为人体工程学。

第6节提供了有关选择和配置垃圾收集器的建议。 它还提供有关如何处理OutOfMemoryErrors的一些建议。 第7节简要介绍了可用于评估垃圾收集性能的一些工具,第8节列出了与垃圾收集器选择和行为相关的最常用的命令行选项。 最后,第9节提供了指向本文涵盖的各个主题的更详细文档的链接。

2. 显式内存管理VS自动内存管理

内存管理是这样的过程:识别何时不再需要分配的对象,释放此类对象使用的内存并使其可用于后续分配。 在某些编程语言中,内存管理是程序员的责任。 该任务的复杂性导致许多常见错误,这些错误可能导致程序意外或错误的行为并崩溃。 结果,开发人员通常会花费大量时间来调试和尝试纠正此类错误。

具有显式内存管理的程序中经常发生的一个问题是悬挂引用(dangling references )。 可以重新分配某个对象使用的空间,而其他对象仍然具有该对象的引用。 如果具有该(悬挂的)引用的对象尝试访问原始对象,但是该空间已被重新分配给新对象,则结果将是不可预测的,而不是预期的结果。

显式内存管理的另一个常见问题是空间泄漏。 当分配内存并且不再引用但不释放内存时,就会发生这些泄漏。 例如,如果您打算释放链表使用的空间,但是犯了一个错误,那就是仅仅重新分配链表的第一个元素,其余的列表元素将不再被引用,但是它们超出了程序的访问范围,因此不能使用或回收。 如果发生足够多的泄漏,它们可以继续消耗内存,直到耗尽所有可用内存为止。

现在,尤其是大多数现代的面向对象的语言普遍采用的另一种内存管理方法是通过称为垃圾收集器的程序进行自动管理。 自动内存管理可增强接口的抽象性和更可靠的代码。

垃圾回收避免了悬挂引用的问题,因为仍然在某处引用的对象将永远不会被垃圾回收,因此不会被视为无效。 垃圾回收还解决了上述的空间泄漏问题,因为它会自动释放不再引用的所有内存。

3. 垃圾回收概念

垃圾收集器负责

  • 分配内存
  • 确保所有引用的对象都保留在内存中,并且
  • 回收由执行代码中的引用无法访问的对象使用的内存

被引用的对象被称为存活对象。不再引用的对象被视为无效对象,或称为垃圾。查找和释放(也称为回收)这些对象使用的空间的过程称为垃圾收集。

垃圾回收解决了许多但不是全部的内存分配问题。例如,你可以无限期创建对象并继续引用它们,直到没有可用的内存为止。垃圾收集也是一项复杂的任务,需要花费时间和资源。

用于组织内存以及分配和释放空间的精确算法由垃圾收集器处理,并向程序员隐藏。通常是从称为堆的大型内存池中分配空间的。

垃圾收集的时间取决于垃圾收集器。通常,整个堆或堆的一部分会在堆满或达到占用率的百分比时收集。

完成分配请求的任务很困难,这涉及到在堆中找到一定大小的未使用内存块。大多数动态内存分配算法的主要问题是避免碎片化(请参阅下文),同时保持分配和释放效率。

理想的垃圾收集器特征

垃圾收集器必须既安全又全面。也就是说,永远都不能错误地释放活动数据,并且垃圾保留时间不能超过垃圾回收周期。

还希望垃圾收集器高效运行,不能引起长时间暂停导致应用程序不能运行。但是,与大多数与计算机相关的系统一样,通常在时间,空间和频率之间进行权衡。例如,如果堆大小很小,则收集会很快,但是堆会更快地填满,因此需要更频繁的收集。相反,大堆将需要更长的时间来填充,因此收集的频率将降低,但可能需要更长的时间。

另一个理想的垃圾收集器特性是碎片限制。当用于垃圾对象的内存被释放时,可用空间可能会在各个区域中以小块的形式出现,从而使得在任何一个连续区域中可能没有足够的空间用于分配大型对象。消除碎片的一种方法称为压缩,下面将在各种垃圾收集器设计选择中进行讨论。

可伸缩性也很重要。分配不应成为多处理器系统上多线程应用程序的可伸缩性瓶颈,并且收集也不应成为此类瓶颈。

设计选择

在设计或选择垃圾收集算法时,必须做出许多选择:

  • 串行vs并行

    使用串行收集,一次只发生一件事。 例如,即使有多个CPU可用,也仅利用一个CPU来执行收集。 当使用并行收集时,垃圾收集的任务分为多个部分,并且这些子部分在不同的CPU上同时执行。 同步操作可以使收集更快地完成,但要付出一些额外的复杂性和潜在的分散性。

  • 并发vsSTW

    当执行STW垃圾收集时,在收集过程中将完全暂停应用程序的执行。 相反的,可以与应用程序同时执行一个或多个垃圾回收任务。 通常,并发垃圾收集器会同时执行其大部分工作,但有时也可能不得不执行一些短暂的停顿。 STW垃圾收集比并发收集更简单,因为堆被冻结并且在收集期间对象没有更改。 其缺点是某些应用程序被暂停可能是不希望的。 相应地,当同时执行垃圾收集时,暂停时间会缩短,但是收集器必须格外小心,因为它对可能由应用程序同时更新的对象进行操作。 这给并发收集器增加了一些开销,这会影响性能并需要更大的堆大小。

  • 整理vs复制

    垃圾收集器确定内存中的哪些对象是活动对象以及哪些是垃圾后,它可以整理内存,将所有活动对象一起移动并完全回收剩余的内存。整理后,可以在第一个空闲位置轻松快速地分配一个新对象。可以使用简单的指针来跟踪下一个可用于对象分配的位置。与整理收集器相反,非整理收集器只释放了垃圾对象利用的空间,即,它不会像整理收集器一样移动所有活动对象以创建较大的回收区域。好处是可以更快地完成垃圾收集,但缺点是可能会产生碎片。通常,从具有只释放的堆中分配要比从整理堆中分配更为昂贵。可能有必要在堆中搜索足够大的内存连续区域以容纳新对象。第三种选择是复制收集器,该复制收集器将活动对象复制(或撤离)到不同的存储区域。这样做的好处是可以将源区域视为空的,并且可用于快速方便的后续分配,但是缺点是复制需要额外的时间以及可能需要的额外空间。

性能指标

利用多种指标来评估垃圾收集器的性能,包括:

  • 吞吐量:未花费在垃圾回收的时间与总时间的百分比
  • 垃圾收集开销:吞吐量的倒数,即垃圾回收所花费时间与总时间的百分比
  • 暂停时间:发生垃圾收集时停止执行应用程序的时间
  • 收集频率:相对于应用程序执行,收集发生的频率
  • 占用量:大小的度量,例如堆大小
  • Promptness:从对象变成垃圾到内存可用之间的时间。

交互式应用程序可能需要很短的暂停时间,而总的执行时间对非交互式应用程序更重要。 实时应用程序需要在垃圾回收暂停和在任何时间花费在回收器中的时间比例上的较小上限。 占用空间小可能是在小型个人计算机或嵌入式系统中运行的应用程序的主要问题。

分代回收

当使用一种称为分代收集的技术时,内存将分为几代,即保存不同年龄对象。 例如,使用最广泛的配置有两代:一个用于年轻对象,一个用于老对象。

可以使用不同的算法在不同的分代中执行垃圾收集,每种算法都是基于针对该特定世代的通常观察到的特性进行优化的。 分代垃圾收集利用以下观察结果(称为弱分代假设),涉及以几种编程语言(包括Java编程语言)编写的应用程序:

  • 大部分分配的对象都不会被引用很长时间,也就是说,它们死得很快
  • 从旧对象到年轻对象的引用很少

年轻代的收集相对频繁,并且高效且快速,因为年轻一代的空间通常很小,并且可能包含许多不再引用的对象。

许多年轻代中幸存下来的对象最终被提升或保留给老年代。 请参见图1。老年代通常比年轻代大,并且其占用率增长更慢。 结果就是,老年代的回收频率低,但需要花费更长的时间才能完成。