垃圾收集器

252 阅读10分钟

序言

本文将会介绍市场上常用的垃圾收集器,掌握这些垃圾收集器各自的特点和执行原理,对于需求不同的系统选择合适的垃圾收集器至关重要。

经典垃圾收集器

垃圾收集器是真正回收垃圾的执行者,在 HotSpot 虚拟机中对于堆中不同的区域所使用的垃圾收集器也不同。如下图所示:

垃圾收集器.drawio.png 图中展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。

Serial 收集器

Serial 收集器是一个单线程收集器,这里单线程不是说明它只会使用一个处理器或者一条收集线程工作,更多的是强调它进行垃圾收集器,必须暂停其他所有的工作线程。其工作示意图如下: image.png 优点:

  • 对比于其他的垃圾收集器,实现方式简单高效
  • 所以垃圾收集器中内存消耗最小的

缺点:

  • 必须暂停用户线程的运行,导致程序停顿

ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程并行版本,除此之外,其余的行为包括 Serial 收集器可用的所有控制参数都一模一样,其工作示意图如下:

image.png ParNew 收集器是 JDK 7 之前的遗留系统中首选的新生代收集器,其中一个与功能、性能无关但很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。 优点:

  • 采用多线并行,来回收不可达对象,减少了垃圾回收所需要的时间

缺点:

  • 回收过程依然需要暂停用户线程

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一款新生代收集器,它是基于标记复制算法实现的收集器,也是能够并行运行的多线程收集器。Parallel Scavenge 收集器与其他收集器关注点不同,CMS 等收集器的关注点是能够尽可能的缩短用户线程停顿的时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。即:

吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)吞吐量 = 运行用户代码时间/(运行用户代码时间 + 运行垃圾收集时间)

Parallel Scavenge 收集器提供了两个参数用于精确设置吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数和直接设置吞吐量大小的 -XX:GCTimeRatio 参数。

Parallel Scavenge 收集器收集器还一个参数 -XX:+UseAdaptiveSizePoliccy。这是一个开关参数,当这个参数被激活之后,就不需要再去指定新生代的大小、Eden与Survivor的比例等细节参数了,虚拟机会根据当前系统的运行状态动态,调节这些参数以提供最合肥的停顿时间或者最大的吞吐量。自适应调节是Parallel Scavenge收集器与别于ParNew 收集器的一个重要特性。其工作示意图如下:

image.png 优点:

  • 采用多线并行,来回收不可达对象,减少了垃圾回收所需要的时间

缺点:

  • 回收过程依然需要暂停用户线程

Parallel Old 收集器

Parallel Old 收集器是Parallel Scavenge 收集器的老年代版本,其是基于标记整理的算法实现的。除了两款收集器作用的区域的不同,其它方面大多相同,不做赘述。

CMS 收集器

CMS 收集器是一种以获取最短回收停顿时间为目标的收集器,它的运行过程较为负责,总体可分为以下四个阶段:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

其中初始标记、重新标记这两个步骤依然需要暂停用户线程。初始标记只是简单的标记一下 GC Roots直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图过程,这个过程耗时长但是不需要暂停用户线程,可以和垃圾收集器线程一起并发运行。重新标记阶段则是为了修正并发标记期间,因用户线程程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段暂停时间比初始标记阶段稍长一点,但远比并发标记时间段短。;最后是并发清除阶段,清除掉已经被判定死亡的对象,这个阶段可以和用户线程并发运行。

由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,CMS收集器内存回收与用户一起并发执行的,大大减少了暂停时间。

工作示意图如下:

image.png

优点:

  • 用户停顿时间大大减少

缺点:

  • 其是基于标记清除的算法实现的,存在内存碎片化问题。

G1 收集器

G1收集器将堆内存划分多个大小相等的独立区域(Region),并且能预测暂停时间,能预测原因它能避免对整个堆进行全区收集。G1跟踪各个Region里的垃圾堆积价值大小(所获得空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证了在有限时间内获得更高的收集效率。 G1收集器工作工程分为4个步骤,包括:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

初始标记与CMS一样,标记一下GC Roots能直接关联到的对象。并发标记从GC Root开始标记存活对象,这个阶段耗时比较长,但也可以与用户线程并发执行。而最终标记也是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录。最后在筛选回收阶段对各个Region回收价值和成本进行排序,根据用户所期望的GC暂停时间来执行回收。

G1 收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,所以,它并非纯粹的追求低延迟,而是在延迟可控的情况下,获得尽可能高的吞吐量。工作示意图如下:

image.png 优点:

  • 实现了在延迟可控的情况下,获得更高的吞吐量

低延迟的垃圾收集齐全

ZGC 收集器

待后续完善

垃圾收集器常用参数

垃圾收集是Java虚拟机(JVM)中管理内存的重要机制,它负责自动回收不再使用的对象所占用的内存空间。以下是一些常用的垃圾收集参数,这些参数可以帮助调优JVM的垃圾收集行为:

一、JVM常规参数

  1. -Xms:设置Java堆的初始大小。
  2. -Xmx:设置Java堆的最大值。
  3. -Xmn:设置Java堆新生代的大小。
  4. -Xss:设置每个线程的栈大小。

二、垃圾收集器选择参数

  1. -XX:+UseSerialGC:开启Serial垃圾收集器,适用于单CPU或较小应用内存的场景,是Client模式下的默认值。
  2. -XX:+UseParNewGC:开启ParNew垃圾收集器,是Serial GC的多线程版本,但JDK9后已取消此参数。
  3. -XX:+UseConcMarkSweepGC:开启CMS(Concurrent Mark Sweep)垃圾收集器,适用于需要低停顿时间的场景,但JDK9后已部分废除相关参数。
  4. -XX:+UseParallelGC:开启Parallel Scavenge垃圾收集器,适用于多CPU、需要高吞吐量的场景,是Server模式(JDK9前)下的默认值。
  5. -XX:+UseParallelOldGC:配合Parallel Scavenge使用Parallel Old垃圾收集器,用于老年代。不过该参数在JDK1.5之后已无用,因为Parallel Scavenge默认使用Parallel Old作为其老年代收集器。
  6. -XX:+UseGIGC(或 -XX:+UseG1GC):开启G1(Garbage-First)垃圾收集器,适用于大内存、多处理器的环境,能够动态调整堆内存的使用,减少停顿时间。JDK9后,G1成为Server模式下的默认值。
  7. -XX:+UseShenandoahGC:开启Shenandoah垃圾收集器,需要配合-XX:+UnlockExperimentalVMOptions使用,在Oracle JDK中不支持。
  8. -XX:+UseZGC:开启ZGC垃圾收集器,同样需要配合-XX:+UnlockExperimentalVMOptions使用,适用于超大堆内存的场景。

三、内存区域大小相关参数

  1. -XX:NewRatio:设置新生代与老年代的比例。例如,设置为3表示新生代占堆的1/4,老年代占3/4。
  2. -XX:SurvivorRatio:设置新生代中Eden区与Survivor区的比例。默认为8,即Eden区占80%,两个Survivor区各占10%。
  3. -XX:PretenureSizeThreshold:设置晋升老年代的对象大小。大于此值的对象直接在老年代分配。
  4. -XX:MaxTenuringThreshold:设置对象晋升老年代的最大年龄。默认为15,即对象在新生代经过15次GC后仍未被回收,则晋升到老年代。

(注意:对于G1垃圾收集器,新生代和老年代的大小是动态调整的,因此-Xmn、-XX:NewRatio等参数对G1无效。)

四、垃圾收集器性能通用参数

  1. -XX:MaxGCPauseMillis:设置垃圾收集的最大停顿时间。JVM会尽量满足这个要求,但可能会牺牲吞吐量或新生代大小。
  2. -XX:GCTimeRatio:设置GC时间占总时间的比率。默认为99,即允许最大1%的GC时间。若无法满足,可能会调整新生代大小。
  3. -XX:+UseAdaptiveSizePolicy:开启自适应大小调整策略。被激活后,JVM会根据应用的需求动态调整新生代大小、Survivor区比例等参数。

五、并发和并行参数

  1. -XX:ParallelGCThreads:设置并行GC时使用的线程数。默认与CPU个数相等。
  2. -XX:ConcGCThreads:设置并发标记、并发整理的执行线程数。对于不同的收集器,这个参数的含义可能有所不同。

六、G1垃圾收集器特有参数

  1. -XX:G1HeapRegionSize:设置G1垃圾收集器中的Region大小。取值范围通常为1MB到32MB,且应为2的N次幂。
  2. -XX:InitiatingHeapOccupancyPercent:设置触发G1垃圾收集周期的堆占用率阈值。默认为45%。
  3. -XX:-G1UseAdaptiveConcMarkSteps 和 -XX:-G1UseAdaptiveIConcRefinementSteps:禁用G1的并行压缩步骤,以减少GC停顿时间但可能增加CPU使用率。

七、其他参数

  1. -XX:HandlePromotionFailure:设置是否允许担保失败。在Minor GC之前,检查老年代是否有足够的空间容纳晋升的对象。如果不允许担保失败且空间不足,则改为进行Full GC。
  2. -XX:+HeapDumpOnOutOfMemoryError:在出现内存溢出异常时,让JVM生成当前的内存堆转储快照,以便进行事后分析。

这些参数可以单独使用,也可以组合使用,以满足不同的应用场景和性能需求。在实际应用中,建议根据应用的特性和性能要求,进行参数调优和测试。

总结

本文介绍了目前市场上用的较多的垃圾收集器,每个收集器所负责的区域和功能不同,各位需要区别选择。对于 ZGC 收集器,由于其是新型的垃圾收集器同时也比较复杂,目前在 JDK 8 中 还暂无支持,所以该内容后续完善(其实我还没理解),最后总结了一些常用的参数,大家在开发的过程中可以酌情选择。