Su的技术博客

  • 首页
  • 原创
  • 视频
  • Java
  • MySQL
  • DDD
  • 事故复盘
  • 架构方案
  • AI
  • Other
  • 工具
    • AI工具集
    • 工具清单
    • JSON在线格式化
    • JSON在线比较
    • SQL在线格式化
  • 打赏
  • 关于
路很长,又很短
  1. 首页
  2. Java
  3. 正文
                           

【转载】一文看懂”ParNew+CMS”组合垃圾回收器

2023-10-13 6593点热度 0人点赞 1条评论

因为工作的需要,笔者前前后后分别接触了HBase RegionServer、HiveServerMetastore以及HDFS NameNode这些大内存JVM服务。 在和这些JVM系统打交道的过程中,GC优化始终是一个绕不过去的话题,有的是因为GC导致NameNode RPC请求耗时增大,有的是因为GC导致RegionServer/HiveServer/Metastore经常宕机。在优化的过程中,笔者花时间系统地学习并梳理了CMS、G1GC以及ZGC这几款垃圾回收器的原理,并基于这些原理进行了多次线上GC问题的定位以及优化。这个系列的文章初步安排了多篇:

 

  1. 【大内存服务GC实践】- 一文看懂”ParNew+CMS”组合垃圾回收器
  2. 【大内存服务GC实践】- “ParNew+CMS”组合垃圾回收器实践案例(一)
  3. 【大内存服务GC实践】- “ParNew+CMS”组合垃圾回收器实践案例(二)
  4. 【大内存服务GC实践】- 一文看懂G1垃圾回收器
  5. 【大内存服务GC实践】- G1垃圾回收器实践案例(一)
  6. 【大内存服务GC实践】- G1垃圾回收器实践案例(二)
  7. 【大内存服务GC实践】- 一文看懂ZGC垃圾回收器
  8. 【大内存服务GC实践】- ZGC垃圾回收器实践案例

 

这是这个系列的第一篇文章,我们来聊聊”ParNew+CMS”垃圾回收器是怎么工作的。Java开发工程师同学肯定对这个组合垃圾回收器有所了解,因为这通常是面试内容的一部分。根据笔者的面试经验,大多数面试者是停留在比较基础、初级的理论知识上面的。无论是关于ParNew还是CMS垃圾回收器,网上其实有很多相关的介绍,但笔者觉得大部分都比较零散,没有系统完整地对其进行介绍。希望这篇文章能够比较全方位地、逻辑清晰地、深入地将”ParNew+CMS”垃圾回收器介绍清楚。

从我们的认知说起

面试”ParNew+CMS”相关知识,面试官一般会从这几个简单的问题开始:
  1. JVM内存为什么要分代?
  2. 新生代GC触发条件是什么?简单介绍一下新生代GC算法。
  3. 在哪些条件下对象会从新生代晋升到老年代?
  4. 老年代GC触发条件是什么?简单介绍一下老年代GC算法。
  5. FGC触发条件是什么?
  6. 如果一个Java系统(CMS回收器,下同)新生代GC耗时长,可以考虑从哪些方面分析优化?
  7. 如果一个Java系统(CMS回收器,下同)老年代GC耗时长,可以考虑从哪些方面分析优化?
  8. 如果一个Java系统频繁发生FGC,可以考虑从哪些方面分析优化?
这些问题粗粗一看还是容易回答的,但是如果面试官深入地问其中的一些细节,比如”CMS回收算法中Card Table的作用主要有哪些?”,估计会难倒不少同学。另外,诸如6、7、8这三个实践类的问题,更是Java系统优化诊断专家的试金石。 那这篇文章我们就深入地看看前面5个理论性质的问题,接下来一两篇文章再通过几个真实大数据生产线上的案例介绍”ParNew+CMS”组合回收器的优化实践。

JVM堆为什么要分代?

分代的垃圾回收策略,是基于两个假设:
  1. 不同对象的生命周期是不一样的。可以大体分成两类,一类称为短寿对象,这类对象存活时间很短,比如局部变量、短链接对象。与之对应的称为长寿对象,比如数据缓存、session对象等。
  2. 大部分Java应用中短寿对象占比都占绝大多数,这类对象可以很快就会被回收。
基于这两个事实假设,大多数GC算法都采用分代回收机制。将JVM堆划分成两个区域,一个小的新生代,一个大的老年代。新生代放置短寿对象,可以采用比较频繁的回收策略,每次可以回收掉大量的垃圾对象。老年代放置长寿对象,可以采用与新生代不同的回收策略。

新生代GC触发条件是什么?简单介绍一下新生代GC算法?

应用程序在Eden区生成新对象,一旦Eden区满了之后就会触发新生代GC,新生代GC算法使用复制算法。复制算法对Eden区以及S0区的对象进行标记,标记出活跃对象,然后将活跃对象复制到S1区。复制算法不会产生内存碎片。对于标记算法中如何判断一个对象是活跃的还是不活跃的(垃圾对象),现在一般使用可达性分析算法。
对吧,90%以上的同学都会这么回答。但这里面有很多细节没有讲清楚,比如标记使用的可达性分析算法是什么算法?具体如何标记一个对象?将活跃对象复制到S1之后,引用这些对象的指针如何变化?这些问题再深入地问下去,能回答出来的就少之又少了,但是理解这些基本知识是接下来深入理解G1ZGC的基础,所以非常有必要在这里进行一番介绍。

如何判断一个对象是否活跃?

判断对象是否活跃目前一般使用可达性分析算法。通过一系列称为”GC Roots”的元素作为起始点,从这些节点开始向下搜索,当一个对象到GC Roots没有任何引用链相连时,则说明此对象是不活跃的。
“GC Roots”是什么?它本质上是一组活跃的引用,注意不是对象。容易理解的有线程栈上的引用变量、静态变量到对象的引用,在分代算法中从非收集区域指向收集区域对象的引用等。
新生代GC是Stop-The-World(以下简称STW)的,即只有标记线程工作,这样就可以将新生代堆想象成一个引用链快照,不会有应用线程去修改这个引用链,这样就可以使用深度优先算法进行引用遍历。JVM中具体的实现算法是三色标记算法,演示图(来自网上,下面相关图相同)如下:
一文看懂”ParNew+CMS”组合垃圾回收器
我们把遍历对象图过程中遇到的对象,按”是否访问过”这个条件标记成以下三种颜色:
  • 白色:尚未访问过。
  • 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问过了。
  • 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问完。全部访问后,会转换为黑色。
假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:
  1. 初始时,所有对象都在【白色集合】中。
  2. 将 GC Roots 直接引用到的对象挪到 【灰色集合】中。
  3. 从灰色集合中获取对象:
(1)将本对象引用到的其他对象全部挪到 【灰色集合】中。
(2)将本对象挪到【黑色集合】里面。
  1. 重复步骤3,直至【灰色集合】为空时结束。
  2. 结束后,仍在【白色集合】的对象即为 GC Roots 不可达,可以进行回收。
当STW时,对象间的引用是不会发生变化的,可以轻松完成标记(这句话重点记得,下文还会提到)。如下图所示,标记完成后为A、D、E、F和G节点都变成了黑色,B、C和H都是白色,表示B、C、H三个对象GC Roots不可达,为垃圾对象:
一文看懂”ParNew+CMS”组合垃圾回收器

如何标记一个活跃对象?

通过三色标记法可以找到所有的活跃对象,那怎么标记这些活跃对象呢?目前主要使用位图标记活跃对象,堆中每个对象都有一个对应的位图,如果是活跃对象,该位图设置为1,否则设置为0。

如何跨区迁移对象?

到目前为止,通过三色标记法我们找到了新生代中所有活跃对象,并将对应的位图进行了标记。接着,我们需要将这些活跃对象从Eden/S0区移动到S1区。整个移动过程分为如下3个子步骤:
  1. 在S1区为对象申请特定大小的内存。
  2. 初始化对象,并将对象的字段进行赋值。
  3. 全部迁移完成之后将Eden/S0区的所有对象清除。

如何修改引用对象指针地址?

迁移完成之后还有一个问题,既然这些活跃对象从一个地方迁移到了另一个地方,那所有引用这些对象的对象指针就需要相应地修改,对吧?那这里就有一个问题,引用新生代对象的目标对象怎么找到呢?
分析一下,这些要找的目标肯定不在新生代,因为新生代存活的对象已经迁移了。那目标对象肯定存在于老年代,也就是有一些老年代的对象引用了新生代的对象,后者地址发生变化之后就要通知前者进行指针变化。问题来了,如何精确找到老年代中的哪个对象引用了新生代的对象呢?

有一个方案是扫描整个老年代的所有对象,但是这样的效率必然不高,尤其是在老年代内存设置非常大的情况下效率就会更差。于是JVM算法设计者设计了使用一个Card Table记录老年代对象到新生代对象的引用方案。

这里多说一句,通常有两种方法记录对象之间的引用关系,一种为Point Out,一种为Point In。假设有这样的引用关系,对象A的成员变量指向对象B(伪代码为:ObjA.Field = ObjB),对于Point Out的记录方式来说,会在对象A(ObjA)的Card Table中记录对象B(ObjB)的地址。对于Point In的记录方式来说,会在对象B(ObjB)的Card Table中记录对象A(ObjA)的地址,这相当于一种反向引用。这二者的区别在于处理时有所不同:Point Out方式在记录这种引用关系的时候比较简单,但是在反向查找时需要对Card Table做全部扫描。Point In记录引用关系操作相对稍微复杂,但是在标记扫描时可以直接找到有用和无用的对象,不需要进行额外的扫描,因为Card Table里面的对象可以看作根对象。”ParNew+CMS”组合回收器中老年代到新生代的跨代引用使用的是Point Out模式。

那Card Table是个什么数据结构呢?我们将老年代这个连续的堆内存空间划分成连续的512Byte内存块,Card Table是一个连续的数组,数组的每个元素大小是1Byte,分别映射老年代堆内存的512Byte空间。如果老年代的某个对象产生了跨代引用,就将对应Card Table上的数组元素标记为Dirty,这个Card就是所谓的Dirty Card。如下示意图所示:
一文看懂”ParNew+CMS”组合垃圾回收器
有了Card Table,就不需要扫描整个老年代空间,而只需要扫描Dirty Cards对应的堆空间,遍历这些堆空间的对象进行必要的调整即可,这样可以大大提升扫描的效率,调整完成之后将对应的Dirty Card重置。

Card Table上的Card什么时候会被设置为Dirty?

这里需要引入一个非常关键的概念 – 写屏障。写屏障可以简单理解为一个钩子函数,对一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑。JVM通过写屏障维护Card Table,如果某一个老年代对象引用一个新生代对象,在将引用赋值写入到内存之前,会执行一段特定代码将老年代对象所在内存区域对应的Card设置为Dirty。没错,这听起来就像AOP。

在哪些条件下对象会从新生代晋升到老年代?

  1. 躲过15次新生代GC后晋升到老年代(15是默认情况)。
  2. 大对象直接进入老年代。
  3. 动态对象年龄判断机制:假如当Survivor区中,相同年龄的对象总大小大于这Survivor区域总大小的50%,那么大于等于这批对象年龄的对象,在下次YGC后就会晋升到老年代。
  4. YGC后存活对象太多超过Survivor区大小,通过分配担保机制晋升到老年代。

老年代GC触发条件是什么?简单介绍一下老年代GC算法?

老年代使用内存占老年代实际大小比例超过一定阈值(可以通过参数-XX:CMSInitiatingOccupancyFraction配置)之后会触发老年代GC。老年代GC使用并发标记清理算法,算法分为初始标记、并发标记、预清理、可中断的预清理、再标记、并发清理以及并发重置状态等7个步骤,其中初始标记、再标记以及并发清理3个阶段是STW。下面是一段完整老年代GC的日志片段:
一文看懂”ParNew+CMS”组合垃圾回收器

现在我们深入地分析一下这些步骤:

   1. 初始标记。从GC Roots集合以及新生代对象出发,标记直接引用的对象。示意图如下所示:

一文看懂”ParNew+CMS”组合垃圾回收器

   2. 并发标记。从初识标记阶段标记出来的对象开始基于”三色标记法”找出所有存活对象并标记。这个阶段应用线程会和标记线程并发执行。假如此时只有标记线程工作,那并发标记前后的示意图如下:

一文看懂”ParNew+CMS”组合垃圾回收器

然而实际上,应用线程会和标记线程一起并发执行,这就可能出现如下几种情况:
  • 对象引用被删除。
  • 应用线程直接在老年代分配新对象。
  • 新生代对象晋升到老年代。
  • 老年代对象之间引用发生变更。
这样的话,并发标记完成后可能不是上面的示意图,而是如下这种示意图:
一文看懂”ParNew+CMS”组合垃圾回收器
分别来看这几种情况:
  • 对象引用被删除(上图场景1):假如一个对象在被标记为活跃对象之后引用关系被删除。因为该对象已经被标记为活跃对象,所以它不会在本次GC中被回收。但是理论上来讲,这个对象是应该被回收的。应该被回收的对象没有被回收,这种情况不影响正确性,但会产生”浮动垃圾”。这种现象称为“多标”。
  • 直接在老年代分配新对象(上图场景2):如果在标记线程执行结束之后应用线程重新new了一些新对象(比如大对象)并产生了引用关系。这些对象本应该被标记为活跃对象但实际上没有被标记,就会出现正确性问题。这是“并发标记”引入的第一个问题。
  • 新生代对象晋升到老年代(上图场景3):因为应用线程在工作,所以Eden区就可能会满,进而触发YGC,YGC之后就会有新生代对象晋升到老年代。晋升到老年代的对象本应该被标记为活跃对象但实际上没有被标记,就会出现正确性问题。与场景2类似。
  • 老年代对象之间引用发生变更(上图场景4):这个场景稍微复杂一点,使用下面的示意图分析一下。
一文看懂”ParNew+CMS”组合垃圾回收器

 

假设标记线程已经遍历到对象B(回想三色标记法,对象B变为灰色),这个时候应用线程执行了如下代码:

 

objB.fieldC = null; 
objA.filedC = C;

 

对应示意图中对象B和对象C之间的引用关系断掉,然后对象A和对象C之间建立新的引用关系。注意对象A和对象C之间虽然建立了新的引用关系,但是对象A已经是黑色了,不会再重新做遍历处理了。最终导致的结果就是:对象C会一直是白色,最后被当作垃圾清理掉。很显然,这直接影响到了应用程序的正确性,是不可接受的。这种现象称为“漏标”,这是“并发标记”引入的第二个问题。

可见,并发标记可以让应用线程与标记线程一起工作,不需要STW。但是会引入如下两个问题:

  • 新增老年代对象没有被标记。
  • 引用变更导致的漏标问题。
   很显然,这些新增对象必须被重新标记上。那怎么重新标记这些新增的对象呢?
   对于场景2,只能重新扫描GC Roots。
   对于场景3,只能重新扫描新生代对象。

   对于场景4,如果在引用变更的时候记录下对应的对象,比如上述场景如果能够记录下对象A,重新标记的时候从对象A开始重新标记,就可以相对快速的完成重新标记。实际实现中再次用到了上文介绍到的Card Table,当引用发生变更时,将对象A所在的Card标记为Dirty。后续只需扫描这些Dirty Cards的对象,避免扫描整个老年代。

   这里有一个问题,如果在老年代并发标记的过程中同时发生了一次YGC。上文我们说过YGC会扫描Card Table中的Dirty Cards,找到跨代引用,同时在YGC完成后将Dirty Cards清空。很显然,增量更新和YGC这两个过程共用Card Table会产生冲突,一旦YGC完成之后将某个Dirty Card清空,但是这个Dirty Card刚好是”并发标记”过程中引用变更标记的,就会导致漏标。为了解决这个问题,CMS算法引入另一种数据结构Mod Union Table,它是一个位数组,数组中每个元素分别对应一个Card。基于这个新结构,在每次YGC处理完脏卡之前,会将该Dirty Card在Mod Union Table中对应的数组位置1。这样CMS在执行重新标记阶段的时候,扫描Mod Union Table和Card Table里面被标记的项,找到所有可能的Dirty Card。

   3. 并发预处理。

   4. 可中断预处理。

这两个阶段都是为了尽可能降低重标记阶段的耗时,采用增量更新的方式重新标记”并发标记”阶段新增的对象。这两个阶段依然是应用线程和标记线程并发执行的,所以还是会有新增对象产生,不过数量会降低很多。

5. 重标记。

经过上面两个阶段的预处理之后,需要重新标记的新增对象理论上应该不是很多了。这个阶段采用STW模式,最后一次标记遗留的新增对象:
  • 遍历GC Roots,标记直接关联的没有被标记的老年代对象以及引用链上的对象。
  • 遍历新生代对象,标记直接关联的没有被标记的老年代对象以及引用链上的对象。
  • 遍历老年代的Dirty Cards,重新标记。
“并发标记-预处理-重标记”这个过程,类似于我们使用Distcp工具迁移一个不断写入的表。通常使用如下策略:
  • 使用distcp全量拷贝一次数据。这个过程distcp和业务写入并发进行。(对应并发标记)
  • 使用distcp -update增量拷贝一次或者多次数据。这个过程distcp和业务写入并发执行。(对应并发预处理)
  • 经过上述两个步骤之后,可以认为需要增量拷贝的数据已经不多了。这个时候暂停写入,再使用distcp -update增量拷贝一次就完成了表的迁移。(对应重标记)

通过这种方式迁移对业务的影响应该是最低的。

6. 并发清理。

经过上述一系列的标记之后,没有被标记的对象就一定是垃圾对象。这些垃圾对象会被并发清理释放内存空间。

   7. 并发重置。

进行Card Table等数据结构的重置等,为下一次GC做准备。

FGC触发条件是什么?

CMS垃圾回收器中FGC一旦发生,就会暂停所有应用线程,并退化成单线程进行垃圾回收,整个暂停耗时非常之长。CMS垃圾回收器一般有两种FGC触发条件:
  1. Concurrent Mode Failure模式FGC。上文我们讲过老年代使用内存占总堆大小超过阈值-XX:CMSInitiatingOccupancyFraction的话就会触发老年代GC。老年代GC的”并发标记”阶段是应用线程和标记线程一起工作的,假如在并发标记的过程中,不断有对象晋升到老年代最终导致老年代内存放不下这些对象的话,就会触发Concurrent Mode Failure模式FGC。根据字面意思也可以猜到这种FGC和并发执行有关系。
  2. Promotion Failure模式FGC。从字面意思来看是晋升失败,是一次新生代GC之后部分对象要晋升到老年代,但是老年代没有足够内存容纳这些对象导致FGC。通常来说,是因为老年代存在大量的内存碎片导致这种模式的FGC。

本文总结

这篇文章系统介绍了CMS垃圾回收器相关的理论知识,主要从实现原理层面解释了如下几个常见问题:
  1. CMS算法为什么要分代?
  2. 其中新生代GC触发条件是什么?简单介绍一下新生代GC算法。
  3. 在哪些条件下对象会从新生代晋升到老年代?
  4. 老年代GC触发条件是什么?简单介绍一下老年代GC算法。
  5. FGC触发条件是什么?
接下来一两篇文章将会基于本篇文章介绍CMS垃圾回收器在大数据生产线上的多个优化实践案例。

参考文章

https://segmentfault.com/a/1190000037752307?utm_source=tag-newest
https://zhuanlan.zhihu.com/p/105495961
https://www.cnblogs.com/jmcui/p/14165601.html
https://www.zhihu.com/question/287945354/answer/458761494
https://zhuanlan.zhihu.com/p/71058481

https://www.jianshu.com/p/2a1b2f17d3e4

本文(https://verysu.com)仅供学习!所有权归属原作者。侵删!文章来源:作者 http://hbasefly.com/2022/01/13/gc-practise-parnew-cms-metastore-fullgc/

更多文章:

  1. ParNew+CMS 实践案例 (一)- NameNode YGC诊断优化
  2. 一文看懂G1GC垃圾回收器
  3. ParNew+CMS 实践案例 : HiveMetastore FullGC诊断优化
  4. JVM GC问题定位排查方法综述
  5. 高并发场景下JVM调优实践之路
  6. G1GC垃圾回收器实践案例
  7. 生产环境的CMS垃圾回收,一定要这样配置参数
  8. Chrome插件(扩展)开发全攻略2.6w字,看这篇就够了!
  9. JVM 内存分析工具 MAT 的深度讲解与实践——进阶篇(长文)
  10. JVM垃圾回收器CMS原理与调优
标签: 转载 GC CMS Java 垃圾回收器 性能调优 FGC ParNew
最后更新:2023-10-28

秋天0261

关注Java领域,后端开发、Netty、Zookeeper、Kafka、ES、分布式、微服务、架构等。分享技术干货,架构设计,实战经验等。

打赏 点赞
< 上一篇
下一篇 >

文章评论

  • 秋天0261

    并发清理并不会STW

    2023-10-16
    回复
  • razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
    取消回复

    广告
    文章目录
    • JVM堆为什么要分代?
    • 新生代GC触发条件是什么?简单介绍一下新生代GC算法?
    • 在哪些条件下对象会从新生代晋升到老年代?
    • 老年代GC触发条件是什么?简单介绍一下老年代GC算法?
    • FGC触发条件是什么?
    • 本文总结
    • 参考文章
    最新 热点 推荐
    最新 热点 推荐
    干货 | 论Elasticsearch数据建模的重要性 马蜂窝消息总线——面向业务的消息服务设计 基于 MySQL Binlog 实现可配置的异构数据同步 视频笔记:Google发布Agent2Agent协议 视频笔记:什么是微服务,为什么是微服务? 视频笔记:什么是AI 智能体? 视频笔记:什么是Flink? 如何秒级实现接口间“幂等”补偿:一款轻量级仿幂等数据校正处理辅助工具
    Elasticsearch 使用误区之六——富文本内容写入前不清洗基于 MySQL Binlog 实现可配置的异构数据同步马蜂窝消息总线——面向业务的消息服务设计干货 | 论Elasticsearch数据建模的重要性你可以不用RxJava,但必须得领悟它的思想!如何秒级实现接口间“幂等”补偿:一款轻量级仿幂等数据校正处理辅助工具视频笔记:什么是Flink?视频笔记:什么是AI 智能体?
    JVM 内存分析工具 MAT 的深度讲解与实践——入门篇 9.包和命名空间(译) 8. EBI 架构(译) 搞懂六边形架构、洋葱架构、整洁架构 架构师日记-从技术角度揭露电商大促备战的奥秘 JVM 内存分析工具 MAT 的深度讲解与实践——高阶篇 阿里云香港云服务器P0史诗级宕机事件复盘 高并发场景下JVM调优实践之路

    CRUD (1) Event Sourcing (1) graphql (1) id (1) NoSQL (1) quarkus (1) rest (1) RocketMQ (2) Spring Boot (1) zk (1) zookeeper (1) 上下文 (1) 事务消息 (1) 二级缓存 (1) 值对象 (1) 关系数据库 (1) 分布式缓存 (1) 原子性 (1) 唯一ID (1) 商品 (1) 多对多 (1) 子域 (1) 字符集 (1) 客户端心跳 (1) 幂等 (2) 干货 (1) 并发 (1) 应用场景 (1) 应用架构图 (1) 康威定律 (2) 异步复制 (1) 微服务架构 (2) 总体方案 (1) 技术方案 (2) 技术架构 (2) 技术架构图 (1) 技能 (1) 持续集成 (1) 支撑域 (1) 故障恢复 (1) 数据架构图 (1) 方案选型 (1) 日记 (1) 服务发现 (1) 服务治理 (1) 服务注册 (2) 机房 (1) 核心域 (1) 泄漏 (1) 洋葱架构 (1) 消息队列 (5) 源码剖析 (1) 灰度发布 (1) 熔断 (1) 生态 (1) 画图工具 (1) 研发团队 (1) 线程 (2) 组织架构 (1) 缓存架构 (1) 编码 (1) 视频 (18) 读写分离 (1) 贵州 (1) 软件设计 (1) 迁移 (1) 通用域 (1) 集群化 (1) 雪花算法 (1) 顺序消息 (1)

    推荐链接🔗
    • AI工具集
    • 工具箱🛠️

    COPYRIGHT © 2014-2025 verysu.com . ALL RIGHTS RESERVED.

    Theme Kratos Made By Seaton Jiang

    粤ICP备15033072号-2

    x