垃圾回收器的选择与配置

一、垃圾回收器介绍

标题为 一、垃圾回收器介绍
垃圾回收器优点缺点针对特性常见组合方式
Serial简单高效单线程导致停顿较长新生代单线程,使用标记-复制算法,适合客户端模式Serial(新生代) + Serial Old(老年代)
ParNew多线程,与CMS配合效果较好仍有Stop-The-World暂停新生代多线程,使用标记-复制算法,CMS的新生代替代者,可与CMS搭配ParNew(新生代) + CMS(老年代)
Parallel多线程,适合高吞吐量停顿较长,可能影响服务质量新生代多线程,并行垃圾收集,吞吐量优先Parallel Scavenge(新生代) + Serial Old(老年代)
CMS并发收集,短暂停顿,适合响应敏感应用内存碎片,吞吐量相对较低老年代并发收集,标记-清除算法,减少停顿时间ParNew(新生代) + CMS(老年代)
G1并发收集,短暂停顿,高吞吐量初始标记和最终标记阶段的停顿较长新生代和老年代分代收集,基于标记-整理算法,根据堆内存划分为多个区域,按照区域优先级执行垃圾回收,适用于大堆内存场景G1(新生代和老年代)
ZGC极短暂停时间,适用于超大堆内存相比G1的吞吐量略低新生代和老年代并发收集,标记-整理算法,目标是将停顿时间限制在10ms以内,适用于大堆内存场景ZGC(新生代和老年代)
Shenandoah极短暂停时间,适用于超大堆内存初始标记和最终标记阶段的停顿较长新生代和老年代并发收集,标记-整理算法,使用许多技术减少停顿时间,适用于大堆内存场景Shenandoah(新生代和老年代)
Epsilon短暂停顿,用于性能测试等场景不进行任何垃圾回收仅做垃圾分配,不进行任何垃圾回收操作,用于性能测试和性能分析-

1.1 Serial 收集器:最早的新生代收集器

标题为 1.1 Serial 收集器:最早的新生代收集器

Serial 收集器是 Java 虚拟机最早的新生代收集器,它采用单线程工作方式。由于其单线程特性,进行垃圾收集时必须暂停所有工作线程,这被称为 “Stop The World” 现象。这样的停顿会导致用户体验变差,因为在停顿期间应用程序无法响应用户操作。

适用场景

  • 客户端模式:适用于 Java 虚拟机运行在客户端模式下,例如桌面应用程序。在这种环境下,内存资源一般较为有限,且用户对停顿时间的容忍度较高。Serial 收集器提供较好的单线程收集效率,并对内存的额外消耗较小,因此是一个合理的选择。

  • 单核处理器或处理器核心数较少:在处理器核心数较少的环境下,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。这对于一些资源受限的设备或环境来说是有意义的。

  • 临时性应用:对于一些临时性应用或测试环境,内存分配并不会很大,而且停顿时间较短的情况下,Serial 收集器也是一个合理的选择。

尽管 Serial 收集器被认为是比较简单且效率高的收集器,但由于其”Stop The World”现象,使用场景存在一定的限制。在某些要求对停顿时间极为敏感的应用场景中,可能需要考虑其他收集器的选择。

1.2 ParNew 收集器:多线程并行的新生代垃圾收集器

标题为 1.2 ParNew 收集器:多线程并行的新生代垃圾收集器

ParNew收集器是HotSpot虚拟机中的新生代垃圾收集器,它是Serial收集器的多线程并行版本。主要适用于以下环境:

  1. 服务端模式:在服务端模式下,应用程序通常需要处理更大量的数据和更高的并发请求。ParNew收集器能够充分利用多核处理器,通过多线程并行的方式提高垃圾收集的吞吐量,从而提升整体性能。

  2. 多核处理器:由于ParNew收集器采用多线程并行方式,它在多核处理器上表现更优秀。它能够充分利用多个处理器核心同时进行垃圾收集工作,从而提高收集效率,特别适合多核服务器环境。

  3. 低延迟要求:相较于Serial收集器,ParNew收集器在并行收集的情况下,可以更快地完成新生代的垃圾收集,从而减少垃圾收集导致的停顿时间,降低对应用程序的影响。

  4. 与CMS收集器配合:在过去,ParNew收集器常与CMS收集器(Concurrent Mark-Sweep)搭配使用。ParNew负责新生代的垃圾收集,而CMS负责老年代的并发收集。这种搭配在特定场景下能够提供较好的垃圾收集性能和较低的停顿时间。

然而,随着垃圾收集技术的进步,更先进的垃圾收集器如G1(Garbage First)的出现,逐渐取代了ParNew收集器。G1收集器是一种面向全堆的垃圾收集器,不需要其他新生代收集器的配合。从JDK 9开始,ParNew收集器和CMS收集器已经不再是官方推荐的垃圾收集器解决方案。

综合而言,ParNew收集器在特定场景下仍然是一个合理的选择,尤其是在早期的Java版本和特定的应用场景中。然而,对于大多数现代应用,更推荐使用更先进的垃圾收集器,如G1收集器,以获得更好的性能和更低的停顿时间。

1.3 Parallel Scavenge 收集器:注重吞吐量的新生代垃圾收集器

标题为 1.3 Parallel Scavenge 收集器:注重吞吐量的新生代垃圾收集器

Parallel Scavenge收集器是HotSpot虚拟机中的新生代垃圾收集器,与ParNew收集器表面上看起来非常相似,也都是基于标记-复制算法实现的多线程并行收集器。然而,Parallel Scavenge收集器在关注点上有一些特别之处,其主要目标是提高吞吐量而非停顿时间。

吞吐量是处理器用于运行用户代码的时间与处理器总消耗时间的比值。Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,即最大限度地提高虚拟机的运算能力,使其能够高效完成程序的运算任务。因此,Parallel Scavenge收集器适用于那些后台运算而不需要太多与用户交互的任务,通过并行进行垃圾收集来尽可能地提高系统的吞吐量。

为了实现可控制的吞吐量,Parallel Scavenge收集器提供了两个参数:

  1. -XX:MaxGCPauseMillis:该参数允许设置一个最大的垃圾收集停顿时间,收集器将尽力保证垃圾收集的时间不超过用户设定值。但需要注意,缩短停顿时间可能会导致垃圾收集更频繁,进而降低吞吐量。

  2. -XX:GCTimeRatio:该参数设置垃圾收集时间占总时间的比率,相当于吞吐量的倒数。默认值为99,即允许最大1%的垃圾收集时间。设置较小的值会增加垃圾收集的频率,从而增加吞吐量,但也可能会增加总体的垃圾收集时间。

Parallel Scavenge收集器的另一个特点是使用自适应调节策略(GC Ergonomics)。启用该策略后,虚拟机会根据当前系统的运行情况动态调整新生代的大小、Eden与Survivor区的比例等细节参数,以提供最合适的停顿时间或最大的吞吐量。这样的调节方式使得内存管理的优化任务交给虚拟机自行完成,对于那些不太了解收集器运作或难以手动优化的用户来说,是一个很方便的选择。

综上所述,Parallel Scavenge收集器是一个专注于吞吐量的垃圾收集器,适用于后台运算而不需要太多与用户交互的任务。通过调节相关参数,用户可以控制垃圾收集器的吞吐量,以满足不同的应用需求。自适应调节策略是Parallel Scavenge收集器的一个重要特点,使得内存管理的优化更加便捷和高效。

1.4 Serial Old 收集器:单线程老年代收集器

标题为 1.4 Serial Old 收集器:单线程老年代收集器

Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。它主要用于供客户端模式下的HotSpot虚拟机使用。然而,在服务端模式下,Serial Old收集器可能有两种用途:

  1. 与Parallel Scavenge收集器搭配使用:在JDK 5以及之前的版本中,新生代通常与Parallel Scavenge收集器搭配使用,而老年代则使用Serial Old收集器。这样的组合在一些场景下可能表现良好,但对于高级的运行环境,吞吐量可能不如其他组合,如ParNew加CMS。

  2. 作为CMS收集器失败的后备预案:Concurrent Mode Failure(并发模式失败)是指在CMS并发收集过程中,由于老年代空间不足,导致无法继续并发地执行垃圾收集。当CMS无法继续并发收集时,系统会触发一次后备的Full GC,并且此时会退化使用Serial Old收集器来完成垃圾收集。这样做虽然导致了停顿时间的增加,但是保证了垃圾收集的完成。

需要说明的是,Parallel Scavenge收集器的架构中本身有PS MarkSweep收集器来进行老年代收集,而不是直接调用Serial Old收集器。但是,这个PS MarkSweep收集器与Serial Old的实现几乎是一样的,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解。

综上所述,Serial Old收集器是一个单线程的老年代收集器,通常适用于客户端模式下的虚拟机。在服务端模式下,它可以与Parallel Scavenge收集器搭配使用,也可以作为CMS收集器失败的后备预案。在后备预案中,它会被用来完成垃圾收集,以保证系统的稳定性。

1.5 Parallel Old 收集器:多线程并发的老年代收集器

标题为 1.5 Parallel Old 收集器:多线程并发的老年代收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。直到JDK 6时才开始提供,之前的情况下,新生代的Parallel Scavenge收集器在一些情况下可能会显得尴尬。原因是在选择了Parallel Scavenge收集器作为新生代收集器后,只有Serial Old(PS MarkSweep)作为老年代收集器的选择,而其他表现良好的老年代收集器(如CMS)无法与Parallel Scavenge配合工作。

在此之前,由于Serial Old收集器是单线程收集器,可能会在一些高级运行环境中拖慢服务端应用性能。因此,选择Parallel Scavenge作为新生代收集器并不一定能够实现整体吞吐量最大化的效果。另外,由于Serial Old收集器的单线程收集,无法充分利用服务器多处理器的并行处理能力,导致在老年代内存空间很大且硬件规格高级的运行环境中,该组合的总吞吐量可能不如ParNew加CMS的组合。

直到JDK 6中引入了Parallel Old收集器,吞吐量优先的收集器终于有了一个名副其实的搭配组合。在注重吞吐量或者处理器资源较为稀缺的场合,可以优先考虑使用Parallel Scavenge加Parallel Old收集器的组合。这样的组合充分利用了多线程并发收集的优势,从而在一些场景下可以取得较好的性能表现。

综上所述,Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,适用于那些注重吞吐量或者处理器资源较为稀缺的场合。这样的组合在一些情况下可以获得较好的性能表现。

1.6 CMS 收集器:最小化垃圾回收停顿时间的并发收集器

标题为 1.6 CMS 收集器:最小化垃圾回收停顿时间的并发收集器

CMS(Concurrent Mark Sweep)收集器是一种旨在最小化垃圾回收停顿时间的Java垃圾收集器。它适用于对服务端应用程序的响应时间要求较高的场景,特别是那些运行在互联网网站或基于浏览器的B/S系统的服务端。

CMS收集器使用标记-清除算法实现,并且其回收过程分为四个步骤:

  1. 初始标记(CMS initial mark):在这个阶段,收集器会标记GC Roots能直接关联到的对象。这个阶段需要停顿用户线程,但它的速度较快。

  2. 并发标记(CMS concurrent mark):在并发标记阶段,收集器从GC Roots的直接关联对象开始遍历整个对象图,标记所有可达的对象。这个阶段不需要停顿用户线程,可以与垃圾收集线程一起并发执行,从而减少停顿时间。

  3. 重新标记(CMS remark):重新标记阶段的目标是修正并发标记过程中由于用户程序继续运作而导致的标记变动。这个阶段的停顿时间通常比初始标记稍长,但仍然远比并发标记阶段的时间短。

  4. 并发清除(CMS concurrent sweep):在并发清除阶段,收集器清理并删除标记阶段判断为已经死亡的对象。由于不需要移动存活对象,这个阶段也可以与用户线程同时并发执行。

整个过程中,耗时最长的并发标记和并发清除阶段垃圾收集器线程可以与用户线程一起工作,使得CMS收集器的内存回收过程与用户线程并发执行,从而降低停顿时间。

尽管CMS收集器有很多优点,例如并发收集和低停顿,但它也有一些明显的缺点:

  • 对处理器资源敏感:CMS收集器占用一部分线程(或处理器计算能力),这可能导致应用程序的总吞吐量下降,特别是在处理器核心数较少的情况下。

  • 无法处理浮动垃圾:由于并发标记和清除期间仍会有新的垃圾对象产生,这些垃圾对象无法在当前收集中处理,而必须留待下一次垃圾收集时再清理,称为”浮动垃圾”。这可能导致”Concurrent Mode Failure”失败,并触发另一次完全”Stop The World”的Full GC。

  • 空间碎片问题:CMS收集器使用标记-清除算法,回收结束时会产生大量空间碎片。这可能导致大对象分配困难,而不得不提前触发一次Full GC。

为了解决这些问题,CMS收集器提供了一些参数进行配置,例如可以调整触发垃圾收集的阈值、启用内存碎片的合并整理过程等。然而,尽管CMS收集器在过去是一款优秀的选择,但自从JDK 9开始,它已被标记为废弃。这意味着在新版本的Java中,建议使用其他更先进的垃圾收集器,例如G1(Garbage-First)收集器、ZGC(Z Garbage Collector)或Shenandoah收集器,以获得更好的性能和更低的停顿时间。

1.7 Garbage-First (G1) 收集器:可预测停顿时间的面向局部收集器

标题为 1.7 Garbage-First (G1) 收集器:可预测停顿时间的面向局部收集器

Garbage-First (G1) 收集器是Java垃圾收集器技术的重要进展,它开创了面向局部收集的设计思路和基于Region的内存布局形式。G1的目标是实现可预测的停顿时间模型,使其成为一款适用于服务端应用的垃圾收集器。

G1收集器最初作为实验功能出现在JDK 7的Early Access版本,并在JDK 7 Update 4正式成为商用级别的垃圾收集器。它在JDK 9发布后取代了Parallel Scavenge加Parallel Old收集器,成为服务端模式下的默认垃圾收集器,而CMS收集器则被标记为不推荐使用并最终退出历史舞台。

G1收集器的设计思路和特点包括:

  1. 基于Region的内存布局:G1将Java堆划分为多个大小相等的Region,每个Region可以扮演不同角色,如Eden空间、Survivor空间或老年代空间。这种动态划分使得G1能够更灵活地处理不同区域的垃圾回收。

  2. 局部收集:G1跳出了传统分代收集的范畴,它可以面向Java堆内存的任意部分进行局部回收,根据Region中垃圾的数量和收集效益来优先处理垃圾回收,实现了”Garbage First”的原则。

  3. 大对象处理:G1引入了Humongous区域来存储大对象,大于一个Region容量一半的对象被视为大对象。超大对象则存放在连续的Humongous Region中,这些区域被视为老年代的一部分。

  4. 可预测的停顿时间:G1收集器通过维护Region的回收价值和优先级列表,根据用户设定的停顿时间目标(参数-XX:MaxGCPauseMillis)来优先回收收益最大的Region,从而实现可预测的停顿时间模型。

G1收集器的出现为垃圾收集器的设计带来了新的思路和可能性。它在一定程度上实现了实时Java(RTSJ)的软实时垃圾收集器特性,对于服务端应用以及注重系统响应时间的场景有着很好的应用价值。随着不断的发展和优化,G1收集器在当今的Java应用中已经成为一款优秀的垃圾收集器选择。

1.8 Shenandoah收集器:面向低停顿时间的垃圾收集器

标题为 1.8 Shenandoah收集器:面向低停顿时间的垃圾收集器

Shenandoah是一种面向低停顿时间的垃圾收集器,它是OpenJDK项目中的一部分,旨在改善Java应用程序的垃圾收集性能。垃圾收集是Java虚拟机(JVM)中的一个重要功能,用于管理内存中不再使用的对象,并回收它们所占用的内存空间,以供后续的对象分配使用。

Shenandoah垃圾收集器的主要目标是将垃圾收集的停顿时间尽可能地缩短,从而减少应用程序的暂停时间。这对于那些对低延迟和高响应性有要求的Java应用程序特别重要。与传统的垃圾收集器不同,Shenandoah不会在垃圾收集过程中长时间地停止应用程序的执行。

Shenandoah的工作过程主要包括以下几个阶段:

  1. 初始标记(Initial Marking):标记与GC Roots直接关联的对象,这是一个短暂的“Stop The World”阶段,与堆大小无关,只与GC Roots的数量相关。

  2. 并发标记(Concurrent Marking):遍历对象图,标记全部可达的对象。与用户线程一起并发进行,时间长短取决于存活对象的数量和对象图的复杂程度。

  3. 最终标记(Final Marking):处理剩余的SATB(Remembered Set)扫描,并确定回收集。也会有一个短暂的停顿。

  4. 并发清理(Concurrent Cleanup):清理那些整个区域内没有存活对象的Region(Immediate Garbage Region)。

  5. 并发回收(Concurrent Evacuation):将回收集中的存活对象复制到其他未使用的Region中。在此过程中,Shenandoah需要解决并发移动对象与用户线程并发访问旧对象的问题。

  6. 初始引用更新(Initial Update Reference):建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成对象移动任务。

  7. 并发引用更新(Concurrent Update Reference):与用户线程一起并发地修正所有指向复制后的新对象的引用。

  8. 最终引用更新(Final Update Reference):修正GC Roots中的引用。

  9. 并发清理(Concurrent Cleanup):回收回收集中的Immediate Garbage Regions。

Shenandoah使用了一种叫做”Brooks Pointers”的转发指针(Forwarding Pointer)来解决并发回收阶段中的对象移动问题。这允许Shenandoah在复制对象的同时,并发地更新用户线程中的引用,从而避免了频繁切换到内核态的开销。

与其他垃圾收集器相比,Shenandoah的主要优势在于其能够在较大的堆内存和高吞吐量的应用程序中表现出更低的停顿时间。然而,Shenandoah并不支持分代收集,默认不使用新生代和老年代的划分,而是以一致的方式管理整个堆内存。

总的来说,Shenandoah是为了实现低延迟的垃圾收集而设计的,并通过并发标记、并发回收和并发引用更新等阶段来减少垃圾收集对应用程序的影响。在那些对低停顿时间和高响应性有要求的Java应用程序中,Shenandoah是一款值得考虑的优秀垃圾收集器选择。

1.9 ZGC(Z Garbage Collector)

标题为 1.9 ZGC(Z Garbage Collector)

ZGC(Z Garbage Collector)是一款低延迟的垃圾收集器,于JDK 11中引入,由Oracle公司开发。它的目标是实现极低的停顿时间,尽量减少垃圾收集对应用程序的影响,从而满足那些对低延迟和高响应性有要求的Java应用程序。

ZGC的设计思路和一些核心技术使其在达成低延迟目标上有着独特的优势。以下是ZGC的主要特点:

  1. 动态Region布局: 类似于Shenandoah和G1,ZGC也采用基于Region的堆内存布局。不过,ZGC的Region具有动态性,可以动态创建和销毁,并且支持不同大小的Region,包括小型、中型和大型Region。这种动态布局使得ZGC能够更好地适应不同的堆内存使用情况,从而提供更灵活的垃圾收集策略。

  2. 染色指针: ZGC的核心技术之一是染色指针(Colored Pointer)。染色指针是一种在64位系统中将指针高4位用于存储标志信息的技术。通过染色指针,ZGC可以直接从指针中获取引用对象的标记状态,避免了额外的内存屏障使用,从而减少对程序运行效率的影响。

  3. 自愈特性: 染色指针的另一个优势是具有自愈特性。一旦某个Region中的存活对象被移走,该Region会立即释放和重用,而不必等待所有指向该Region的引用都被修正。这使得ZGC在堆中还有空闲Region的情况下,可以快速完成垃圾收集,从而减少停顿时间。

  4. 限制: 尽管染色指针带来了很多优势,但也有一些限制。例如,内存不能超过4TB,不能支持32位平台和压缩指针。

总体而言,ZGC是一款非常适合低延迟需求的垃圾收集器。它通过动态Region布局和染色指针等技术,实现了极低的停顿时间,并且在适应不同场景的同时提供了出色的性能和扩展性。对于那些对低延迟和高响应性要求较高的Java应用程序,ZGC是一个很好的选择。不过,也需要注意其一些限制和适用范围。

二、选择合适的垃圾回收器

标题为 二、选择合适的垃圾回收器

2.1 常见场景和建议

标题为 2.1 常见场景和建议

2.1.1 延迟敏感的场景

标题为 2.1.1 延迟敏感的场景
  1. 互联网应用:对于需要快速响应和低延迟的互联网应用,建议使用低延迟的垃圾收集器,如G1、Shenandoah或ZGC。它们能够最小化停顿时间,提供更好的用户体验。

  2. 实时系统:对于实时系统和对峰值性能有要求的应用,推荐使用Shenandoah或ZGC。它们的染色指针技术和自愈特性可以显著降低垃圾回收对系统的影响。

2.1.2 吞吐量优先的场景

标题为 2.1.2 吞吐量优先的场景
  1. 批处理任务:如果您的应用程序是批处理任务,且对整体吞吐量较为关注,可以选择并行或并发收集器,如Parallel GC或CMS。这些收集器在高吞吐量场景下表现较好。

  2. 数据处理:大规模数据处理应用通常需要高吞吐量,可考虑Parallel GC或CMS。如果内存较大且延迟要求不高,也可以尝试G1。

2.1.3 内存大小和硬件特性

标题为 2.1.3 内存大小和硬件特性
  1. 小内存应用:对于小内存的应用程序,可以考虑使用Serial GC或G1。Serial GC适用于较小的堆内存,G1在小内存情况下也有不错的表现。

  2. 大内存应用:对于大内存的应用程序,推荐使用Parallel GC、CMS、Shenandoah或ZGC。这些收集器在大内存场景下有较好的性能和扩展性。

  3. 多核处理器:对于多核处理器,Shenandoah和ZGC可能会表现较好,它们能够充分利用多核并发进行垃圾回收。

2.1.4 分代收集和版本选择

标题为 2.1.4 分代收集和版本选择
  1. 分代收集:如果应用程序有明显的新生代和老年代对象的特征,建议选择支持分代收集的垃圾收集器,如G1、Parallel GC或CMS。分代收集器可以针对不同代的对象采取不同的收集策略,提高垃圾回收效率。

  2. 版本选择:尽可能选择最新的JDK版本,以获得垃圾收集器的最新优化和改进。然而,对于实际生产环境,建议谨慎使用实验性质的垃圾收集器,尽量选择经过验证的稳定版本。在新版本的JDK中,不同垃圾收集器可能会有不同的默认选择,需要根据具体需求进行配置。

  1. 根据应用程序的特性和需求,可以尝试调整垃圾收集器的参数。例如,可以通过-XX:GCTimeRatio-XX:MaxGCPauseMillis调整停顿时间和吞吐量的权衡,或者使用-XX:ConcGCThreads来配置并发垃圾收集线程数等。

  2. 调优垃圾收集器的参数需要综合考虑,可能需要进行多次实验和性能测试,找到最适合应用程序的配置。

2.2.1 收集器适用场景表

标题为 2.2.1 收集器适用场景表
垃圾收集器场景
G1互联网应用,小至中型内存,分代收集需求
Shenandoah实时系统,多核处理器,对低延迟要求较高的应用
ZGC实时系统,大内存,对低延迟和高吞吐量的应用
Parallel GC批处理任务,数据处理,大内存,对吞吐量要求较高的应用
CMS批处理任务,大内存,对吞吐量和停顿时间有要求的应用
Serial GC小内存,简单应用,非常低的停顿要求

2.2.2 非准确简易决策树

标题为 2.2.2 非准确简易决策树

下图的决策树是不够准确的,仅作为一个简易直观的参考

Loading graph...