经典垃圾收集器

关于串行、并行、并发的解释可见 垃圾回收的并行与并发

关于垃圾收集器的发展史、分类、性能指标、组合关系可见 垃圾收集器前言

本篇文章主要介绍 7 种经典垃圾收集器:Serial GC、ParNew GC、Parallel Scaverge GC、Serial Old GC、Parallel Old GC、CMS、G1

虽然本篇文章会从很多角度对比不同的垃圾收集器,但并非是为了争个高下找出最好最优秀的垃圾收集器,而是为了挖掘每种垃圾收集器最适合的使用场景

可以通过参数-XX:+PrintCommandLineFlags查看当前使用的垃圾收集器,如:java -XX:+PrintCommandLineFlags -version

Serial / Serial Old GC (串行)

Serial GC

Serial 收集器是最基础、历史最悠久的收集器,曾经 (在 JDK 1.3.1 之前) 是 HotSpot 虚拟机新生代的唯一选择,采取 标记-复制算法

Serial 是一个单线程工作的垃圾收集器,这体现在两个方面:

正是由于它的特点:单线程、串行收集,可能导致用户停顿感强烈,所以 HotSpot 虚拟机开发团队一直致力于消除或者降低用户线程因垃圾收集而导致的停顿时间

吐槽了这么多缺点,但也有它存在的必要。迄今为止,它仍是 HotSpot 虚拟机运行在客户端模式下的默认新生代收集器,与其它收集器的单线程相比它简单高效,这体现在两个方面:

注意:对于 64 位的客户端,默认使用 Parallel Scaverge GC,不过可以使用参数-XX:+UseSerialGC指定使用 Serial 收集器

Serial Old GC

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用 标记-整理算法

Serial Old 收集器的主要意义也是提供客服端模式下的 HotSpot 虚拟机使用;如果是在服务端模式下,它也可能有两种用途:

运行示意图

Serial / Serial Old 收集器运行过程如下图所示:

2

ParNew GC (并行)

先来解释一下名字:Par 是 Parallel 的缩写,表示并行;New 表示收集对象为新生代

ParNew 收集器实质上是 Serial 收集器的多线程版本,除了同时使用多条线程运行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法 (标记-复制)、Stop The World、对象分配规则,回收策略等都与 Serial 收集器完全一致,在实现上这两种收集器也共用了相当多的代码

ParNew 收集器运行过程如下图所示:

3

历史

根据 垃圾收集器的组合关系 部分的介绍,我们来聊一聊 ParNew 收集器的历史!!

虽然 ParNew 收集器和 Serial 收集器相比,只有一个地方不同:支持多线程,但它却是不少运行在服务端模式下的 HotSpot 虚拟机种,尤其是 JDK7 之前的遗留系统中首选 ParNew 作为新生代收集器

之所以这样,有一个与功能、性能无关但其实很重要的原因:除了 Serial 收集器外,目前只有 ParNew 收集器能与 CMS 收集器配合工作

CMS 收集器是 HotSpot 虚拟机种第一款真正意义上支持并发的垃圾收集器,首次实现了让垃圾收集器和用户线程同时工作,可以说直到 CMS 的出现才巩固了 ParNew 的地位,但成也萧何败也萧何!!

随着垃圾收集器技术的不断进步,JDK9 开始完全移除了 ParNew + Serial Old 和 Serial + CMS 的组合,也就是说 CMS 和 ParNew 从此只能相互搭配使用,再也没有其它垃圾收集器能够和它们配合

换个角度可以理解成 ParNew 合并入 CMS,成为专门处理新生代的组合部分,ParNew 可以说是 HotSpot 虚拟机中第一款退出历史舞台的垃圾收集器

对比

聊完历史,我们再来聊一聊 ParNew 收集器一定就比 Serial 收集器效果好吗??

如果是在单核心处理器的环境中,ParNew 收集器绝对不会有比 Serial 收集器更好的效果

如果是在多核心处理器的环境中,ParNew 收集器对于垃圾收集时系统资源的高效利用还是有很好的效果

Parallel Scaverge / Parallel Old GC (并行-吞吐量优先)

Parallel Scaverge GC

Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于 标记-复制算法 实现的收集器,也是能够并行收集的多线程收集器...

它的诸多特性从表面上看和 ParNew 收集器非常像,但特别之处有两个:

Parallel Old GC

Parallel Old 是 Parallel Scaverge 收集器的老年代版本,支持多线程并行收集,基于 标记-整理算法

Parallel Old 收集器直到 JDK6 时才开始提供,在此之前,新生代的 Parallel Scaverge 收集器一直处于相当尴尬的状态

如果新生代选择 Parallel Scaverge 收集器,那么老年代只有 Serial Old 收集器可以与之配合,但由于 Serial Old 收集器在服务端应用性能的拖累,使得 Parallel Scaverge 收集器未必能获得吞吐量最大化的效果,这种组合还不如 ParNew + CMS

直到 Parallel Old 收集器的出现,吞吐量优先收集器终于有了比较名副其实的搭配组合,而 JDK8 中默认垃圾收集器也变成了 Parallel Scaverge GC + Parallel Old GC

这种搭配组合适用于注重吞吐量或者处理器资源较为稀缺的场合,如:在后台运算而不需要太多交互的任务 (批量处理、订单处理、工资支付、科学计算的应用程序)

参数配置

-XX:+UseParallelGC:手动指定年轻代使用 Parallel 并行收集器执行内存回收任务

-XX:+UseParallelOldGC:手动指定老年代使用 Parallel Old 并行收集器执行内存回收任务

-XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般地,最好与处理器核心数相同,避免过多的线程数影响垃圾收集性能 (上下文切换开销),默认值的计算公式如下所示:

ParallelGCThreads={CPU_Core,CPU_Core83+(5×CPU_Core/8),CPU_Core>8

-XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间 (即 STW 的时间),单位是毫秒

-XX:GCTimeRatio:垃圾收集时间占总时间的比例,即:1/(1+N),用于衡量吞吐量的大小

-XX:+UseAdaptivesizePolicy:开启 Parallel Scavenge 收集器自适应调节策略,默认为开启

运行示意图

Parallel Scavenge / Parallel Old 收集器运行过程如下图所示:

4

CMS (并发-低停顿)

CMS (Concurrent Mark Sweep) 收集器和前面介绍的几款都不太一样,这体现在两个方面:

目前很大一部分的 Java 应用集中在互联网网站或者基于浏览器的 B/S 系统的服务端上,这类应用通常都会较为关注服务的响应速度,系统系统停顿时间尽可能的短,以给用户带来良好的交互体验

CMS 是一款老年代收集器,基于 标记-清除算法,这一点也和前面介绍的几款老年代收集器不太一样,它的运作过程也更为复杂一些,整个过程包括四个步骤:

强调:其中初始标记重新标记仍然需要 Stop The World

由于整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从整体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的

CMS 收集器运行过程如下图所示:

5

优点

它的优点很明显:并发收集、低停顿

缺点

虽然优点很明显,但缺点也不容忽视!!

缺点一:对处理器资源非常敏感

在并发标记阶段,虽然垃圾收集器线程可以和用户线程并发执行,但却会因为占用了一部分处理器的计算能力而导致应用程序变慢,总吞吐量降低

CMS 默认启动的回收线程数为 t=(CPU_Core+3)/4

CPU_Core4 时,t/CPU_Core 随着 CPU_Core 的增加而降低,且始终不低于 25%;当 1CPU_Core3 时,t=1

举个极端的例子,当 CPU_Core=1 时,t=1,此时标记阶段和用户线程就退化成串行执行了,且会产生 STW

为了缓解这种情况,虚拟机提供一种称为「增量式并发收集器 (i-CMS)」的 CMS 收集器变种,它是在并发标记、清理的时候让收集器线程和用户线程交替运行,尽量减少垃圾收集器线程的独占资源的时间

这样整个垃圾收集过程会更长 (增加了上下文切换开销),但对用户程序的影响就会显得较少一些,直观感受就是速度变慢的时间多了,但速度下降幅度没有那么明显

实践证明 i-CMS 收集器效果很一般,从 JDK7 开始,i-CMS 模式已经被声明为了 "deprecated",即已过时不在提倡用户使用,到了 JDK9 发布后 i-CMS 模式被完全废弃!!

缺点二:无法处理浮动垃圾

在并发标记阶段,垃圾收集器在对象图上标记颜色,同时用户线程在修改引用关系,这个时候可能出现把原本消亡的对象错误的标记为存活,这些标记错误的对象就被称为浮动垃圾,这种情况可以容忍,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好,详情可见 并发的可达性分析

虽然浮动垃圾不会影响程序的正常运行,但它始终占用了一部分内存,如果浮动垃圾过多,就有可能出现 "Concurrent Mode Failure" 失败进而导致另一次完全 Stop The World 的 Full GC 的产生

由于垃圾收集线程和用户线程同时在执行,所以就不能等到一点内存空间都没有才进行垃圾收集,因为在垃圾收集的过程中用户线程可能会申请新的内存空间,所以必须预留一部分空间供并发收集时的程序使用

在 JDK5 的默认设置下,CMS 收集器当老年代使用了 68% 的空间后就会被激活,这是一个偏保守的设置,可以通过参数-XX:CMSInitiatingOccupancyFraction来自行设置比例

到 JDK6 的时候,CMS 收集器的启动阈值就已经默认提升至 92%,但是太高又会面临另外一种风险:要是 CMS 运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次并发失败 (Concurrent Mode Failure),这个时候虚拟机就需要启动后备预案:冻结用户线程的执行,临时启动 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿的时间就很长了

缺点三:存在内存碎片

由于 CMS 收集器是基于 标记-清除算法 实现的,所以在收集结束时会产生大量空间碎片

空间碎片过多时,会给大对象分配内存带来麻烦,往往会出现本来老年代还有很多剩余空间,但就是无法找到足够大的连续空间为当前对象分配内存,而不得不提前触发一次 Full GC

为了解决这个问题,CMS 提供了两个参数:

小结

到此为止,一共介绍了六种垃圾收集器,根据 垃圾收集器的组合关系 可知,JDK9 之后有且仅有三种组合关系:Serial/Serial Old;ParNew/CMS;Parallel/Parallel Old

G1 (并发-区域化分代式)