Java虚拟机之垃圾收集器


写在前面

本文作为阅读了周志明作者的 <<深入理解Java虚拟机>> 的读书笔记。由于个人理解有限,本文摘抄的内容可能比较片面,强烈建议入手本书!本文还参考了 JDK 8 的相关文档,这有助于我们了解版本之间的差异。本文的大部分内容都来自于文档,这也是本文看起来比较晦涩的原因;仅在此做个记录,下一次回顾本文,将是真正开始使用的时候了。

我们需要知道垃圾收集器的设计者们都是为了什么目标在努力(应用程序的吞吐量和最短的垃圾收集暂停),本文的段落有时候看起来毫无关联,但如果认真阅读,仔细思考的话,你会发现这都是非常重要的知识点,不过由于本人能力有限,只能像堆砌方块一样,将它们罗列在这里。


Java HotSpot VM 包含三种不同类型的收集器,每种收集器具有不同的性能特征。

由于分类设计到了并行和并发两个概念,书中对于并行和并发有这样的描述:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(可能会交替执行),用户程序继续运行,垃圾收集程序则运行于另一个 CPU 上。

垃圾收集采用的多是 “分代收集算法”。根据对象的存活周期不同,可以分为新生代和老年代。新生代由Eden和两个survivor 空间组成。大多数对象最初是在 eden 中分配的。一个 survivor 空间随时都是空的,可作为eden 中任何存活对象的目的地;另一个survivor空间是下一个复制集合期间的目的地。以这种方式在survivor空间之间复制对象,直到它们足够老到可以复制到老年代为止。


Serial Collector

串行收集器使用单个线程来执行所有垃圾收集工作,这使之相对高效,因为线程间没有通信开销。它最适合单处理器计算机,可以使用 -XX:+UseSerialGC 明确启用串行收集器。

该收集器有一个特点,在它进行垃圾收集时,必须暂停其它所有的工作线程,直到它收集结束。它仍然是虚拟机在 Client 模式下的默认选择。

本来想在这里给出测试结果,但发现我电脑上的vm只能运行在 Server 模式下。基于官方文档如何判断JVM是运行在 Client 模式还是Server模式 都能找出答案。


Parallel Collector

并行收集器(也称为吞吐量收集器)并行执行 minor GC, 这可以显著减少垃圾收集开销。它使用于具有在多处理器或多线程硬件上运行的中型到大型数据集的应用程序。可以使用 -XX:+UseParallelGC 明确启用并行收集器。

并行压缩是使并行收集器能够并行执行 major GC 的特性。如果没有并行压缩,major GC 将使用单个线程执行,这将极大地限制可伸缩性。如果指定了 -XX:+UseParallelGC 选项,则默认启用并行压缩。关闭它的选项是 -XX:-UseParallelOldGC

书中指出 minor GC 是发生在新生代的垃圾收集动作,而 major GC 或者 Full GC 是发生在老年代的。


并行收集器是类似于串行收集器的分代收集器。主要区别在于使用多个线程来加速垃圾回收。并行收集器通过命令行选项启用 -XX:+UseParallelGC。默认情况下,使用此选项,minor GCmajor GC 都可以并行执行,以进一步减少垃圾收集的开销。

默认情况下,在服务器级计算机上选择并行收集器。另外,并行收集器使用一种自动调整的方法,该方法允许您指定特定的行为,而不是生成大小和其他低级的调整详细信息。您可以指定最大垃圾收集暂停时间,吞吐量和占用空间(堆大小):

  • 最大垃圾收集暂停时间:使用命令行选项 -XX:MaxGCPauseMillis=<N> 指定最大暂停时间目标。这被解释为需要<N> 毫秒或更少的暂停时间;默认情况下,没有最大暂停时间目标。如果指定了暂停时间目标,则会调整堆大小和其他与垃圾收集相关的参数,以使垃圾收集暂停时间短于指定的值。这些调整可能会导致垃圾收集器降低应用程序的总体吞吐量,并且所需的暂停时间目标不能总是得到满足。

  • 吞吐量:通过花费在垃圾收集上的时间与花费在垃圾收集之外的时间(称为应用程序时间)来度量吞吐量目标。目标由命令行选项 -XX:GCTimeRatio=<N> 指定,该选项将垃圾收集时间与应用程序时间的比率设置为1 / (1 + )。

    例如,-XX:GCTimeRatio=19 设置的目标是垃圾收集总时间的 1/20 或 5%。默认值为99,因此垃圾收集的时间目标为1%。

  • 内存占用:使用选项 -Xmx<N> 指定最大的堆内存占用。此外,收集器还有一个隐含的目标,即只要满足了其他目标,就可以最小化堆的大小。

这些目标将按下列顺序处理:

  1. 最大垃圾收集暂停时间目标
  2. 吞吐量目标
  3. 最小内存占用目标

首先满足最大暂停时间目标。只有在达到这个目标之后,才能实现吞吐量目标。类似地,只有在满足前两个目标之后才会考虑最小内存目标。

Concurrent Collector

并发收集器并发执行大部分工作(例如,当应用程序仍在运行时),以保持垃圾收集暂停时间较短。它是为具有中型到大型数据集的应用程序设计的,其中响应时间比总体吞吐量更重要,因为用于最小化暂停的技术会降低应用程序性能。Java HotSpot VM 提供了两个主要的并发收集器;

  • 使用选项 -XX:+UseConcMarkSweepGC 来启用 CMS 收集器,
  • 使用选项 -XX:+UseG1GC 来启用 G1 收集器。

并发的开销

以并发为主的收集器用更短的major GC暂停时间交换处理器资源(应用程序可以使用这些资源)。除了在并发阶段使用处理器之外,还需要额外的开销来支持并发。因此,虽然并发收集器的垃圾收集暂停时间通常要短得多,但应用程序吞吐量也往往比其他收集器略低。这也解释了我们为什么要在吞吐量以及最短的垃圾收集暂停时间之间做出选择。

因为在并发阶段至少有一个处理器用于垃圾收集,所以并发收集器通常不会在单处理器(单核)计算机上提供任何好处。


并发标记清除(CMS)收集器

此收集器适用于希望使用更短的垃圾收集暂停并能够与垃圾收集共享处理器资源的应用程序

并发标记清除(CMS)收集器是为那些喜欢更短的垃圾收集暂停的应用程序设计的,它们可以在应用程序运行时与垃圾收集器共享处理器资源。通常,具有相对较大的长生命期数据集(一个大型的终身生成)并运行在具有两个或多个处理器的机器上的应用程序将从此收集器的使用中受益。但是,对于任何需要较短暂停时间的应用程序,都应该考虑使用此收集器。CMS 收集器是通过命令行选项 -XX:+UseConcMarkSweepGC 启用的。


与其他可用的收集器类似,CMS收集器是分代的;这样就可能同时发生major收集和minor收集。CMS 收集器尝试通过使用独立的垃圾收集器线程,在应用程序线程执行的同时跟踪可到达的对象,从而减少由于major收集而导致的暂停时间。在每个major收集周期中,CMS收集器在收集开始时暂停所有应用程序线程一小段时间,然后在收集中期再次暂停。第二次停顿往往是两次停顿中较长的停顿。在这两个暂停期间,使用多个线程来执行收集工作。收集的其余部分(包括对活动对象的大部分跟踪和对不可到达对象的清扫)是使用一个或多个与应用程序并发运行的垃圾收集器线程完成的。次要收集可以与正在进行的主循环交叉进行,并以类似于并行收集器的方式进行(特别是,应用程序线程在次要收集期间停止)。


CMS 收集器无法处理浮动垃圾,可能出现 “ Concurrent Mode Failure” 失败而导致另一次 Full GC 的产生

由于 CMS 并发清理阶段用户线程还在运行着,伴随程序的运行自然会有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS 无法在本次收集中处理掉它们,只好留待下一次 GC 时再将其清理掉。这一部分垃圾就称为 “浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,要是预留的内存无法满足程序需要,就会出现一次 “Concurrent Mode Failure” 失败。这会导致应用程序线程停止,等待收集完成。

由于并发模式失败的代价可能非常大,如果保留期代(老年代)的占用超过初始占用(保留期代的百分比),也会开始并发收集。该初始占用阈值的默认值约为 92%,但是该值可能随版本的不同而变化。这个值可以使用命令行选项 -XX:CMSInitiatingOccupancyFraction=<N>,其中<N>是终身生成大小的整数百分比(0到100)。


CMS 收集器并不是完全不会暂停应用程序,在并发收集周期中,它会暂停应用程序两次。第一个暂停是将从根(例如,来自应用程序线程堆栈和寄存器、静态对象等的对象引用)和堆中的其他地方(例如,年轻代)直接访问的对象标记为活动的。第一个暂停称为初始标记暂停。第二个暂停出现在并发跟踪阶段的末尾,在 CMS 收集器完成对对象的跟踪之后,发现由于对象中引用的应用程序线程更新而导致并发跟踪丢失的对象。第二个暂停称为重新标记暂停。

可达对象图的并发跟踪发生在初始标记暂停和重新标记暂停之间。在这个并发跟踪阶段,一个或多个并发垃圾收集器线程可能正在使用处理器资源,否则应用程序就可以使用这些资源。因此,即使应用程序线程没有暂停,在此和其他并发阶段,受计算限制的应用程序可能会看到相应的应用程序吞吐量下降。在重新标记暂停阶段之后,一个并发的清除阶段将收集标识为不可到达的对象。收集周期完成后,CMS 收集器将等待,几乎不消耗任何计算资源,直到下一个主要收集周期开始。


调度暂停

年轻代收集和终身代收集的暂停是独立发生的。它们不重叠,但可以快速地连续发生,这样一个集合的暂停,紧接着另一个集合的暂停,可以表现为单个的、更长的暂停。为了避免这种情况,CMS 收集器尝试将重新标记暂停安排在上一代和下一代暂停之间的中间位置。这种调度目前初始标记暂停还未完成,它通常比重新标记暂停短得多。


G1 (Garbage-First 垃圾收集器)

这个收集器适用于具有大内存的多处理器机器。在实现高吞吐量的同时,高概率满足垃圾收集暂停时间的目标

与CMS一样,G1是为需要更短GC暂停的应用程序设计的。


堆被划分为一组大小相等的堆区域,每个堆区域都有一个连续的虚拟内存范围。G1 执行并发全局标记阶段,以确定整个堆中对象的活动性。标记阶段完成后,G1 知道哪些区域大部分为空。它首先收集这些区域,这通常会产生大量的*空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1 将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。G1 使用暂停预测模型满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。

G1 将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩和释放内存。这种转移是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,每次垃圾收集时,G1 都在不断地减少碎片。这超出了前面两种方法的能力。CMS(并发标记清除)垃圾收集不做压缩。并行压缩只执行全堆压缩,这会导致相当长的暂停时间。

需要注意的是 G1 不是一个实时收集器。满足设定的暂停时间目标的概率高,但不具有绝对的确定性。根据以前收集的数据,G1 估计在目标时间内可以收集多少个区域。因此,收集器对收集区域的成本有一个合理准确的模型,它使用这个模型来确定在停留在暂停时间目标内要收集哪些区域和多少区域。

G1 计划作为并发标记-清除收集器(CMS)的长期替代品。将 G1 与 CMS 进行比较,可以发现 G1 与 CMS 之间的差异,从而使 G1 成为更好的解决方案。一个不同之处在于 G1 是一个压缩收集器。此外, G1 比 CMS 收集器提供了更多可预测的垃圾收集暂停,并允许用户指定所需的暂停目标。

G1 将堆划分为固定大小的区域(灰色框):图片来自官方文档

Java虚拟机之垃圾收集器

从逻辑上说,G1 是分代的。一组空白区域被指定为逻辑年轻代。在图中,年轻一代是浅蓝色的。分配是在逻辑年轻代之外完成的,当年轻代满时,将对该区域集进行垃圾收集(年轻收集)。在某些情况下,可以同时对年轻区域集(深蓝色的旧区域)之外的区域进行垃圾收集。这称为混合集合。在图中,正在收集的区域用红色方框标记。该图说明了一个混合的集合,因为同时收集了年轻区域和老区域。垃圾收集是一个压缩的收集,它将活动对象复制到选定的、最初为空的区域。根据幸存对象的年龄,可以将对象复制到幸存区域(用“S”标记)或复制到旧区域(没有特别显示)。标有“H”的区域内,包含了体积超过半个区域的巨型物体,并经过特殊处理。

标记周期的各个阶段标记周期分为以下几个阶段:

  • 初始标记阶段:G1 GC在此阶段标记根。此阶段由常规(STW)的年轻垃圾回收承载。
  • 根区域扫描阶段:G1 GC扫描在初始标记阶段标记的对老一代的引用的幸存者区域,并标记引用的对象。此阶段与应用程序(而不是STW)同时运行,必须在下一次STW 年轻代垃圾收集开始之前完成。
  • 并发标记阶段:G1 GC在整个堆中找到可访问的(活动的)对象。此阶段与应用程序同时发生,并且可以被STW年轻的垃圾回收中断。
  • 标记阶段:此阶段是STW收集,有助于完成标记周期。G1 GC耗尽SATB缓冲区,跟踪未访问的活动对象,并执行引用处理。
  • 清理阶段:在此最后阶段,G1 GC执行记帐和RSet清理的STW操作。在记帐期间,G1 GC会识别出完全空闲的区域和混合垃圾收集候选对象。清理阶段是部分并发的,它重置并将空区域返回给空闲列表。

总结

这样看来,

  1. 串行收集器主要是在client 模式下启用的,并且单个线程,暂停应用程序,新生代采用复制算法,老年代采用标记-整理算法。
  2. 并行收集器类似于串行收集器,它依旧会暂停应用程序,但使用多线程进行垃圾收集。
  3. 并发收集器则能够在垃圾收集的某些阶段与应用程序共享处理器资源,说实话,对于 G1 收集器,我也并未很明白。