Serial、Serial Old 收集器
Serial 收集器 是最基础、历史最悠久的收集器,这个收集器是一个单线程工作的收集器,它的“单线程”并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。Serial Old 收集器 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用
下图展示了了 Serial 收集器、Serial Old 收集器的运行过程:
虽然如此,但它依然是 HotSpot 虚拟机运行在客户端模式下的默认新生代收集器,它有着优于其他收集器的地方,那就是简单而高效,对于内存资源受限的环境,它是所有收集器里面额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说不会很大,因此垃圾收集的停顿时间完全可以控制在十几、几十毫秒内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以 Serial 收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
# 打开此开关后,使用 Serial + Serial Old 的收集器组合进行内存回收
-XX:+UseSerialGC
ParNew 收集器
ParNew 收集器 实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集外,其余的行为包括 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、对象分配规则、回收策略等都与 Serial 收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew 收集器的工作过程如下图所示:
ParNew 收集器在单核心处理器的环境中绝对不会有比 Serial 收集器更好的效果。当然,随着可被使用的处理器核心数量的增加,ParNew 对于垃圾收集时系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,此外,还可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
ParNew 收集器除了支持多线程并行收集外,其他与 Serial 收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的 HotSpot 虚拟机,尤其是 JDK 7 之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
在 JDK 5 发布时,HotSpot 推出了具有划时代意义的 CMS 收集器。这款收集器是 HotSpot 虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程同时工作。遗憾的是 CMS 作为老年代的收集器无法与 JDK 1.4 中已经存在的新生代收集器 Parallel Scavenge 配合工作,所以在 JDK 5 中使用 CMS 来收集老年代时,新生代只能选择 ParNew(默认)或 Serial 收集器中的一个。
# 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收,在 JDK 9 后不再支持
-XX:+UseParNewGC
# 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收,Serial Old 收集器作为 CMS 收集器出现 "Concurrent Mode Failure" 失败后的备用收集器使用
-XX:+UseConcMarkSweepGC
但随着垃圾收集器技术的不断改进,更先进的 G1 收集器登场。G1 是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。所以自 JDK 9 开始,ParNew 合并入 CMS 成为它专门处理新生代的组成部分。
Parallel Scavenge、Parallel Old 收集器
Parallel Scavenge 收集器 是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器,Parallel Old 收集器 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。Parallel Old 收集器直到 JDK 6 时才开始提供。
Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
为此,Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数,注意这两个参数是互斥的。
- -XX:MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值,但要注意:垃圾收集停顿时间的缩短是以牺牲吞吐量和新生代空间为代价换取的。
- -XX:GCTimeRatio 参数的值应当是一个大于 0 小于 100 的整数,表示垃圾收集时间占总时间的比率,默认为 99%,即允许 1% 的 GC 时间。
除上述两个参数外,Parallel Scavenge 收集器还有一个参数 -XX:+UseAdaptiveSizePolicy 值得我们关注。这是一个开关参数,当这个参数被激活后,就不需要人工指定新生代的大小、Eden 与 Survivor 区的比例、晋升老年代对象大小等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为垃圾收集的自适应调节策略。
执行过程:
启用参数:
# 打开此开关后,使用 Parallel Scavenge + Serial Old 的收集器组合进行内存回收
-XX:+UseParallelGC
# 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
-XX:+UseParallelOldGC
其实自 JDK 7 开始,就对 -XX:+UseParallelGC 默认的老年代收集器进行了改进,改进使得 HotSpot VM 在选择使用 -XX:+UseParallelGC 时会默认开启 -XX:+UseParallelOldGC,也就是说默认的老年代收集器是 Parallel Old。因此在 JDK 8 中默认的选择是:-XX:+UseParallelGC,即 Parallel Scavenge + Parallel Old 组合。
可通过如下命令查看 JVM 参数
java -XX:+PrintFlagsFinal -server
CMS 收集器
CMS 收集器(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器,从名字(包含 Mark Sweep)上就可以看出 CMS 收集器是基于标记-清除算法实现的。它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
1. 垃圾收集过程
1)初始标记:
仅仅只是标记一下 GC Roots 能直接关联到的对象,不向下追溯。这个过程是 STW 的,但由于只是标记第一层,所以速度很快。注意,这里除了要标记相关的 GC Roots 之外,还要标记年轻代中对象的引用。
2)并发标记:
从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,垃圾收集线程可以与用户线程一起并发运行。在这个阶段的执行过程中,对象的引用关系可能会产生变化,在这个阶段受到影响的老年代对象所对应的卡页,会被标记为 dirty,用于后续重新标记阶段的扫描。
3)重新标记
为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,该阶段的停顿时间通常比初始标记阶段稍长,但远比并发标记阶段的时间短。
4)并发清除
清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一 起工作,所以从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
2. 优缺点
CMS 收集器是 HotSpot 虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:
1)资源敏感
首先 CMS 收集器对处理器资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。
2)浮动垃圾
其次,由于 CMS 收集器无法处理浮动垃圾(FloatingGarbage),有可能出现 “Concurrent Mode Failure” 失败进而导致另一次完全 “Stop The World” 的 Full GC 的产生。在 CMS 收集器的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为浮动垃圾。因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
3)空间碎片
最后,由于 CMS 收集器是一款基于 “标记-清除” 算法实现的收集器,这意味着收集结束时会有大量的空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为此,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 参数(默认开启,JDK 9 废弃)强制 JVM 在 FGC 完成后对老年代进行压缩,执行一次空间碎片整理。但因为空间碎片的整理阶段也会引发 STW,为了减少 STW 次数,CMS 还提供了 -XX:+CMSFullGCsBeforeCompaction 参数,可以要求 CMS 收集器在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理。该参数默认值为 0,表示每次进入 Full GC 时都进行碎片整理。