【JVM】垃圾收集器-Serial、Parallel、ParNew、CMS的特性与优缺点总结

  • Serial垃圾收集器
  • Parallel Scavenge垃圾收集器
  • ParNew垃圾收集器
  • CMS垃圾收集器
  • Rset与CardTable
  • 打印GC日志到文件
  • 总结

Serial垃圾收集器

  • Serial垃圾收集器是一个历史非常悠久的垃圾收集器,JDK1.3.1前是HotSpot年轻代垃圾收集的唯一选择;
  • 年轻代采用复制算法,老年代采用标记-整理算法
  • Serial Old垃圾收集器是Serial的老年代版本,它同样是一个单线程收集器;它主要有两大用途:一种用途是在JDK1.5及以前的版本中与Parallel Scavenge垃圾收集器搭配使用,另一种用途是作为CMS垃圾收集器的备选方案(在并发失败后使用,但是尽量避免,性能很低);
  • 开启Serial垃圾收集器的JVM参数:-XX:+UseSerialGC,-XX:+UseSerialOldGC;

缺点

  • Serial垃圾收集器是单线程收集,并且在垃圾收集阶段需要STW,因此在多核CPU与多线程当道的今天,Serial垃圾收集器的垃圾回收效率会相对低下;

优点

  • 实现比较简单,并且在单线程的物理机运行环境下,由于没有多线程交互带来的开销,其单线程收集效率会比较高;

【JVM】垃圾收集器-Serial、Parallel、ParNew、CMS的特性与优缺点总结_第1张图片

Parallel Scavenge垃圾收集器

  • Parallel Scavenge垃圾收集器可以认为是Serial收集器的多线程版本(升级版本),因为除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似;
  • Parallel垃圾收集器默认的收集线程数跟cpu核数相同(逻辑核数),当然也可以用参数(XX:ParallelGCThreads)指定垃圾收集开启的线程数,但是一般不推荐修改;
  • Parallel Scavenge垃圾收集器的目标是高吞吐量,即减少垃圾收集时间(高效的利用CPU),让用户代码获得更长的运行时间;
  • 年轻代采用复制算法,老年代采用标记-整理算法
  • Parallel Old垃圾收集器是Parallel Scavenge垃圾收集器的老年代版本
  • JDK8默认使用Parallel Scavenge垃圾收集器和Parallel Old垃圾收集器
  • 开启Parallel Scavenge垃圾收集器的JVM参数:-XX:+UseParallelGC,-XX:+UseParallelOldGC;

【JVM】垃圾收集器-Serial、Parallel、ParNew、CMS的特性与优缺点总结_第2张图片

ParNew垃圾收集器

  • ParNew垃圾收集器其实跟Parallel垃圾收集器很类似,区别主要在于它可以和CMS收集器配合使用;(Parallel不可以与CMS搭配使用)
  • 年轻代采用复制算法,老年代采用标记-整理算法
  • ParNew与CMS搭配使用(ParNew回收年轻代,CMS回收老年代)是在JDK1.8以及以前的版本中很长一段时间内企业普遍采用的垃圾收集解决方案
  • 开启ParNew垃圾收集器的JVM参数:-XX:+UseParNewGC;

CMS(Concurrent Mark Sweep)垃圾收集器

  • CMS垃圾收集器是HotSpot虚拟机第一款真正意义上的并发垃圾收集器(垃圾收集线程与用户线程并发执行),虽然还是会有STW,但是其耗时非常短,是一款以用户体验至上的垃圾收集器;
  • CMS垃圾收集器的CMS GC并不等同于Full GC,虽然CMS GC是操作的老年代,但是其STW时间才会被真正计入Full GC的时间,这个时间是非常短的;
  • CMS垃圾收集器实现也比较复杂,其垃圾收集可以分为如下几个阶段:
  • ①初始标记:暂停所有的用户线程(STW),并记录下GC Roots直接(只标记根节点)能引用的对象,速度很快;
  • ②并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。因为用户程序并发运行,可能会导致已经标记过的对象的状态发生改变;
  • ③重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序并发运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记;
  • ④并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色(三色标记算法)不做任何处理;
  • ⑤并发重置:重置本次GC过程中的标记数据;

【JVM】垃圾收集器-Serial、Parallel、ParNew、CMS的特性与优缺点总结_第3张图片

优点

  • 适用内存需求特别大的程序运行;
  • 垃圾收集过程中STW的时间非常短,用户体验比较好;

缺点

  • 对CPU资源敏感,对于核心数少的服务器CPU资源占用会非常大,逻辑核心数大于4个后才会改善很多(与用户线程抢占硬件资源);
  • 无法处理浮动垃圾(在并发标记和并发清理阶段又新产生的垃圾,这种浮动垃圾只能等到下一次gc再清理);
  • CMS垃圾收集器使用的垃圾回收算法标记-清理算法会产生大量的内存碎片,当然这可以通过JVM参数打开内存整理功能来解决这个问题;
  • 由于CMS垃圾收集器在并发标记与并发清理阶段与用户线程并发执行,因此可能在并发执行过程中,可能会有新对象被提升到老年代(进入老年代的一些策略),从而引发内存空间不足,导致并发收集失败(concurrent mode failure),此时会立即STW,并用serial old垃圾收集器来回收垃圾,等回收完后再开启用户线程;而serial old垃圾收集器使用单线程,在大内存下执行会非常慢,对程序性能会造成很大的影响;

CMS常见的JVM参数设置

  • -XX:+UseConcMarkSweepGC:开启CMS垃圾收集器;
  • -XX:ConcGCThreads:并发GC线程数;
  • -XX:+UseCMSCompactAtFullCollection:开启CMS GC之后做压缩整理(减少内存碎片);
  • -XX:CMSFullGCsBeforeCompaction:多少次CMS GC后做一个压缩整理(默认:0,代表每次都会做),整理是会消耗系统资源的,如果系统压力只集中在一段时间的话,每次整理也没关系,但是如果系统压力持续很大的话,就不建议每次都做整理了;
  • -XX:CMSInitiatingOccupancyFraction:当老年代使用比例达到多少时会触发CMS GC(单位:百分比,默认92);实际使用中需要仔细评估这个参数的值(比如,大对象比较多就百分比往下调),为了预防并发收集失败的问题;
  • -XX:+UseCMSInitiatingOccupancyOnly:上条参数值(-XX:CMSInitiatingOccupancyFraction)是否允许JVM动态调整(如配置了则不动态调整);
  • -XX:+CMSScavengeBeforeRemark:在CMS GC(针对老年代)前是否进行一次Minor GC;
  • -XX:+CMSParallellnitialMarkEnable:在初始标记时开启多线程执行;
  • -XX:+CMSParallelRemarkEnabled:在重新标记时开启多线程执行;

Rset(Remember Set)与CardTable

  • 当然在这些涉及部分区域的垃圾收集器而言,其都会面临一个比较大的问题,就是跨代引用的问题;比如,在老年代的对象引用年轻代的对象的情况,这时候如果一个年轻代垃圾收集器还要去扫描老年代的GC Root显然不合适,而且性能开销也比较大;Rset(记录从非收集区到收集区的指针集合)就是用来解决这个问题的;
  • 垃圾收集场景中,收集器只需通过Rset判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引用指针的全部细节,也无须扫描非收集区的对象;

HotSpot虚拟机对Rset的实现

  • hotSpot使用一种叫做“卡表”(cardtable)的方式实现Rset,也是目前最常用的一种方式;
  • 卡表是使用一个字节数组实现:"CARD_TABLE[]",每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”(HotSpot虚拟机指定一个卡页为512字节);
  • 一个卡页中可能有多个对象,当一个卡页中有一个元素存在跨代指针(通过元素赋值,写屏障检测跨代指针),那么这个卡页就标记为Dirty(元素值置为1);GC时,会将所有Dirty的卡页中的元素加入GC Root里;

打印GC日志到文件

  • 对于java应用我们可以通过一些配置把程序运行过程中的GC日志全部打印出来,然后分析GC日志得到关键性指标,分析GC原因,对JVM参数进行调优;
//指定GC文件输出路径
‐Xloggc:./gc‐%t.log
//开启打印GC日志
‐XX:+PrintGCDetails
//打印GC的日期与时间
‐XX:+PrintGCDateStamps  
‐XX:+PrintGCTimeStamps
//打印GC原因
‐XX:+PrintGCCause
//使用滚动日志打印,一共20个文件,每个文件100M
‐XX:+UseGCLogFileRotation 
‐XX:NumberOfGCLogFiles=10 
‐XX:GCLogFileSize=100M
  • 打印完的日志文件内容可以导入一些第三方的GC日志分析工具来进行分析,然后给出JVM优化建议,比如:GC Viewer,GCeasy;
  • GCeasy是一个在线的分析工具,分析的维度还比较多,值得推荐,地址是:https://gceasy.io/;

总结

  • 串行垃圾收集器的目标是让整个垃圾收集过程尽可能高效,时间尽可能短,而并发垃圾收集器目标是用户体验,让STW时间尽可能短,但是垃圾收集过程持续的时间是比较长的;
  • 如果不是单核处理器的服务器的话,现在已经不再单独使用Serial垃圾收集器了;
  • Parallel Scavenge垃圾收集器的目标是CPU的高吞吐量,在多线程垃圾收集时会STW(时间相对CMS长很多),适合一些计算量大并且内存需求不是特别大的(一般是小于8GB)的场景;至于STW的时间,我们可以通过调整垃圾收集器的各种参数使得垃圾回收次数和垃圾回收时STW时间尽可能小来提升用户体验。

注意:本文归作者所有,未经作者允许,不得转载

你可能感兴趣的:(JVM,算法,jvm,java,垃圾回收,GC)