Fork me on GitHub

ParNew和CMS垃圾回收器

垃圾收集算法

分代收集理论

JVM采用分代收集的算法,根据对象存活周期的不同将内存分为几块。堆内存分为新生代和老年代,可以根据各个年代的特点来选择合适的算法。

比如在新生代中,每次收集都会大量对象被回收,所以剩余很少,可以采用标记-复制算法,只需要很少的复制成本就可以完成垃圾收集,所以新生代都是采用复制算法。优点是新生代对象大量都被回收,效率高,复制之后内存可以用指针碰撞移动指针分配新的内存,比freelist方式产生空间碎片好的多。复制算法的缺点就是空间利用率会降低,S区只能有一块区域存放存活对象,但是一般Survivor区不会设置太大;还有个缺点就是如果分代年龄阈值设置过大可能会经历多次复制才能晋升到老年代。

而在老年代中,对象存活几率比较大,而且没有额外的空间可以再去晋升或者担保,所以必须选择标记-清除、标记-整理等算法进行垃圾回收。 这种算法比复制算法要慢10倍以上。

Remember Set和cardtable卡表

在新生代做GCRoots可达性扫描过程中可能碰到跨代引用,比如老年代引用了新生代的对象,而如果直接去老年代扫描效率很低,引入Remember Set来记录非回收区到收集区的指针集合,避免把整个老年代加入到扫描的范围。 在很多垃圾收集器都有这样的问题,而Remember Set这样的结构就是解决跨代引用的问题。

cardtable卡表是RemembeSet的具体实现,卡表比如在新生代记录一个字节数组byte[],再将老年代划分多个区域(卡页),字节数组记录对应老年代每个区域是否有跨代引用和地址,如果存在跨代引用,则叫做脏页,这样GC时可以通过卡表只扫描对应脏页内的对象,增加了扫描效率。

image-20220701104506482

标记-复制算法

为了解决效率问题,复制算法出现。将内存划分为大小相同的两块,每次只能用其中的一块。(联想年轻代中的Survivor区)。当这一块内存使用完之后,将GC之后存活的对象复制到另一块,把使用过的空间一次性清理掉,效率高,每次可以直接对一半内存进行直接回收。但是空间利用率低。好在年轻代大多数对象生命周期很短,所以每次GC之后的存活对象很少,所以对Survivor两块区域不需要占用年轻代过大的比例。
image-20220701104515230

标记-清除算法

两个阶段:标记和清除。
标记即为利用GCRoots可达性分析标记存活的对象,统一回收清除未标记(可回收)的对象。
image-20220701104525460

优点:空间利用率高,整块内存都可以使用。
缺点:

  • 效率问题(如果需要标记的对象很多,效率会受影响,且这过程如果伴随STW,对应用程序有影响)
  • 空间问题(在对回收对象清除之后,会产生大量不连续的空间碎片,对下次要求连续内存的分配可能会产生影响)

标记-整理算法

标记过程和标记-清除算法一样,也是标记GCRoots进行可达性分析寻找存活对象,然后在第二部不是直接清除回收垃圾对象,而是先让存活对象向一端移动,整理成连续的内存,然后清理掉剩余的内存。

这个算法标记过程还是可能存在效率问题。但是避免了标记-清除算法中的空间碎片问题,留下的空闲内存都是连续的空间。

image-20220701104535754

垃圾收集器

垃圾收集器是垃圾回收算法的具体实现。主流的垃圾收集器如下:
image-20220701104544488

Serial收集器

Serial(串行)收集器是单线程收集器,在GC时会单线程GC且会STW直到收集结束。

Serial提供了新生代垃圾回收器,采用复制算法,如果使用需要配置参数-XX:+UseSerialGC。同时也提供了老年代的回收器,采用标记-整理算法,需要配置参数:-XX:+UseSerialOldGC。

image-20220701104554689

Serial虽然是单线程的,但是优点是简单且高效(对比其他收集器的单线程版本),比如在单个CPU环境下可能是比较好的选择(不会频繁的上下文切换)。同时Serial Old也会作为CMS垃圾收集器在concurrent mode failure场景下退化的fullgc老年代的垃圾收集器;也可以搭配Parallel Scavenge收集器使用。

ParNew收集器

ParNew是新生代的垃圾回收器,采用复制算法。可以用参数-XX:+UseParNewGC来控制使用ParNew作为新生代的垃圾收集器。如果设置了CMS作为老年代的垃圾收集器,那么默认新生代使用ParNew作为新生代进行搭配。

ParNew就是Serial的多线程版本,有多个线程回收新生代的垃圾对象。同时也是除了Serial之外可以和CMS进行分代配合的收集器,所以很多都选择ParNew+CMS这样的垃圾回收器的组合。

ParNew在进行回收时多个线程并行回收,但是这里还是会STW用户线程的,和Serial一样。可以使用+XX:ParallelGCThreads参数来设置GC线程的个数。其工作过程如下:
image-20220701104614936

垃圾回收器中的并发、并行

在讨论垃圾收集器的上下文语境中,并行和并发如下解释:

  • 并行(parallel):指多个线程并行的去GC,如ParNew新生代垃圾回收器,但是用户线程还是会被暂停STW。
  • 并发(concurrent):指GC线程和用户线程并发执行,不会暂停用户线程的执行,都去分不同的时间片。

Parallel Scavenge/Old收集器

Parallel Scavenge是新生代垃圾回收器,可以使用-XX:+UseParallelGC参数指定使用。其也是采用复制算法、多线程进行parallel的gc。

工作过程和ParNew的多线程进行GC,STW用户线程一致,但在设计上有点区别。

而Parallel Old则是多线程GC的老年代垃圾回收器,采用的是标记-整理垃圾回收算法,同时也是更注重吞吐量,而不是尽力缩短GC的停顿时间。

GC的停顿时间的缩短是牺牲吞吐量和新生代空间来换取的:比如新生代调整小一些,收集肯定更快发生,这样新生代的GC会更频繁。比如原来每10s回收一次新生代,每次停顿100ms,现在就变成了每5s一次新生代回收,每次停顿70ms,吞吐量降低了,但是每次停顿时间变少了。

Parallel Scavenge和ParNew的区别

  • Parallel Scavenge更追求吞吐量(即CPU运行用户线程时间 / (CPU运行用户线程时间 + CPUGC的时间)),对后台计算和CPU敏感型的任务更友好。而ParNew等其他收集器更注重GC的停顿时间,即减少STW时间,对和用户交互的程序更友好。
  • Paralle Scavenge只能和Serial Old或者Parallel Old老年代的回收器搭配使用,不能和CMS搭配使用。而ParNew是可以和CMS搭配使用的。
  • Parallel Scavenge可以自适应调整新生代的Eden、Surrvivor区域的比例,参数是-XX:+UseAdaptiveSizePolicy。

CMS垃圾回收器

CMS(Concurrent Mark Sweep)是老年代的垃圾回收器,其目标是获取最短停顿时间。也是HotSpot实现的真正意义上并发(Concurrent注意不是并行,意味着不会停顿用户线程)收集器,实现了在一定阶段让用户线程和GC线程一起工作。

CMS采用标记-清除算法,但是内部也提供了在清除之后整理防止出现内存碎片的功能和参数。

CMS的步骤过程

CMS每个阶段的工作过程如下:
image-20220701104628660

CMS一定不会STW用户线程吗?

CMS的并发(不STW用户线程)也只是发生在某些阶段的,而不是整个CMS的过程不会STW。比如初始标记和再次重新标记的过程都是会STW的,只不过这两个阶段会占用整个CMS GC的很小一部分时间,最多的还是在并发标记和并发清理阶段。

同时CMS在并发标记或者并发清理阶段,因为不会STW用户线程,可能又触发了old区的垃圾回收,即一次fullgc,那么会出现“Concurrent mode failure”,即会退化为单线程Serial Old垃圾回收器,会单线程的GC,且也会STW。

初始标记(会STW)

初始标记会STW,其实标记一下和GC Roots直接关联到的对象,速度很快。

并发标记(GC线程和用户线程并发、耗时、标记会有误差)

初始化标记之后,会对GCRoots直接关联的对象进行Traceing,即通过可达性分析去寻找引用链上的对象进行标记,这个过程是并发的,即GC线程和用户线程可以同时执行,不会STW。

当然这个阶段很耗费时间,因为是要寻找对象引用的链。

还有这个阶段因为不会STW用户线程,所以标记的对象可能会多标(比如用户线程已经释放了对象关联的GCroots,可能是栈帧中的局部变量),那么这些就会在这个阶段浮动垃圾;也可能少标(用户线程在可达性分析的完成之后,对象又关联上了GCroots的引用)。这部分依赖底层三色标记的算法和后面重新标记的过程来修正或者直接当做浮动垃圾(多标的场景)

预清理阶段

理解CMS回收器的preclean阶段:https://blog.csdn.net/enemyisgodlike/article/details/106960687。
主要是为了后面重新标记而提前清理cardTable(记录跨代引用),和调整最终重新标记的时机。

重新标记(会STW)

重新标记就是为了修正并发标记阶段因为用户线程继续运行导致标记产生变动的对象,这个阶段的时间一般会比初始标记的时间长,但远比并发标记阶段时间短。重新标记阶段会STW(肯定的不然又多标记或者漏标),依赖底层三色标记算法的增量更新算法(JVM中的赋值写屏障)。

并发清理(GC清理和用户线程并发执行)

开启用户线程,和GC线程对未标记(垃圾对象)进行清理,如果设置了整理压缩内存的参数再去整理内存,这个过程对于三色标记法中标记黑色的对象不进行处理。

并发重置

重置对象的一些信息,方便下次GC时重新标记。

CMS总结

优点: 分成多个阶段,其中STW的时间占用很少,最大限度的减少了STW停顿时间,多线程和GC线程进行并发标记和并发清除,加快了效率。

缺点:

  • 对CPU资源敏感。设置GC线程不当或者CPU资源紧张时,多个GC线程切换可能会抢占用户线程资源,使得应用总吞吐量变低,负载变高。
  • CMS在整个过程中会产生浮动垃圾,浮动垃圾即并发标记或者并发清理过程中用户线程可能产生的新的垃圾对象。这部分可以等待下一次gc再进行清理。(因为存在重新标记的过程,这部分浮动垃圾会比较少)
  • CMS因为产生了浮动垃圾,且参数-XX:CMSInitiatingOccupancyFraction参数(老年代占用多少启动CMS GC)默认为92,假设在CMS过程中因为用户线程并发,预留的内存空间不足以容纳程序需要,则会出现Concurrent mode failure,即再进行一次full gc,此时CMS也会退化为Serial Old,即单线程回收,整个过程都STW,效率变得很低。
  • CMS本身采用“标记-清除”算法,这个算法本身可能导致大量的空间碎片,因为用户线程并发,在清理之后可能无法容纳新晋升的大对象的连续内存导致FullGC。JVM提供了清除之后压缩的两个参数:-XX:+UseCMSCompactAtFullCollection(在CMS之后开启压缩整理合并碎片)、-XX:CMSFullGCsBeforeCompaction(执行多少次CMS FullGC之后才进行压缩整理,默认为0,代表每次CMS之后都会整理碎片空间,因为碎片压缩整理也会STW,所以提供了这个值)。

CMS相关核心参数

image-20220701104646352

-------------本文结束感谢您的阅读-------------

本文标题:ParNew和CMS垃圾回收器

文章作者:夸克

发布时间:2020年05月21日 - 00:05

最后更新:2022年07月01日 - 10:07

原始链接:https://zhanglijun1217.github.io/2020/05/21/ParNew和CMS垃圾回收器/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。