JVM 面试必问的 CMS,你懂了吗?

不多废话,今天我们就来盘他。

正文

======

1、什么是卡表(card table)?

试想一下,在进行 YGC 时,如何判断是否存在老年代到新生代的引用?

一个简单的办法是扫描整个老年代,但是这个代价太大了,因此 JVM 引入了卡表来解决这个问题。

卡表又称为卡片标记(card marking),由 Paul R. Wilson 和 Thomas G. Moher 在1989年发表的论文里提出。

其原理为,在逻辑上将老年代空间分割为若干个固定大小的连续区域,分割出来的每一个个区域就称为卡片(card)。另外,为每个卡片准备一个与其对应的标记位,最简单的实现方案是由字节数组实现,以卡的编号作为索引。每个卡的大小通常介于128~512字节之间,一般使用2的幂字节大小,例如HotSpot使用512字节。

当卡片内部发生引用变化时(指针写操作),写屏障会将该卡在卡表中对应的字节标记为脏(dirty)。

有了卡表后,在 YGC 时,只需将卡表中被标记为 dirty 的 card 也作为扫描范围,就可以保障不扫描整个老年代也不会有遗漏了。

2、什么是 mod-union table?

通过上面对 card table 的介绍,我们知道 card table 会记录下老年代所有发生过引用变化对象所在的 card,而 CMS 在并发标记等阶段,也需要记录下老年代发生引用变化的对象以便后续重新扫描,是否可以直接复用 card table?

答案是不行的,这是因为每次 YGC 过程中都涉及重置和重新扫描 card table,这样是满足了 YGC 的需求,但却破坏了CMS的需求,CMS 需要的信息可能被 YGC 给重置掉了。为了避免丢失信息,于是在 card table 之外另外加了一个 Bitmap 叫做 mod-union table。

在 CMS 并发标记正在运行的过程中,每当发生一次 YGC,当 YGC 要重置 card table 里的某个记录时,就会更新 mod-union table 对应的 bit,相当于将 card table 里的信息转移到了 mod-union table 里。

这样,最后到 Final remark 的时候,card table 加 mod-union table 就足以记录在并发标记过程中老年代发生的所有引用变化了。

3、CMS 垃圾收集的过程?

CMS 垃圾收集的过程网上通常有两个版本,4个步骤的和7个步骤的,两个版本其实都是对的。

4个步骤应该主要是跟随周志明的说法,而 CMS 的相关论文其实也是按4个步骤介绍。

7个步骤则应该更多是从 CMS 的日志得出的说法,而7个步骤里其实也包含了上述的4个步骤,可以理解为7个步骤是更细的说法。

个人而言,我会更喜欢7个步骤的说法,因此这边介绍下7个步骤的过程。

1)初始标记(Initial Mark)

STW(stop the world),遍历 GC Roots,标记 GC Root 直达的对象。

2)并发标记(Concurrent Mark)

从初始标记阶段被标记为存活的对象作为起点,向下遍历,找出所有存活的对象。

同时,由于该阶段是用户线程和GC线程并发执行,对象之间的引用关系在不断发生变化,对于这些对象,都是需要进行重新标记的,否则就会出现错误。为了提升重新标记的效率,JVM 会使用写屏障(write barrier)将发生引用关系变化的对象所在的区域对应的 card 标记为 dirty,后续只需要扫描这些 dirty card 区域即可,避免扫描整个老年代。

3)并发预处理(Concurrent Preclean)

该阶段存在的意义主要是为了尽可能降低 Final Remark 阶段的耗时,因为 Final Remark 阶段是 STW 的。

该阶段主要做的事是将上一阶段被标记为 dirty 的 card 所对应的区域进行重新扫描标记,处理并发阶段发生引用变化的对象。

4)可中断的并发预处理(Concurrent Abortable Preclean)

该阶段和并发预处理做的事是基本一样的,也是主要处理 dirty card。区别在于并发预处理只执行一次,而本阶段会一直循环执行,直到触发终止条件。

终止条件有以下几个:

循环次数超过阈值 CMSMaxAbortablePrecleanLoops,默认是0,也就是没有循环次数的限制。

处理时间达到了阈值 CMSMaxAbortablePrecleanTime,默认是5秒。

Eden区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration,默认为50%。

同时该阶段有一个触发前提:

Eden 区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold,默认是2M。

5)最终标记/重新标记(Final Remark)

STW(stop the world),主要做两件事:

遍历 GCRoots,重新扫描标记

遍历被标记为 dirty 的 card,重新扫描标记

6)并发清理(Concurrent Sweep)

清理未使用的对象并回收它们占用的空间。

7)并发重置(Concurrent Reset)

重置 CMS 算法用于打标的数据结构(markBitMap),为下一次收集做准备。

4、CMS存在的问题

1)使用的标记-清除算法,可能存在大量空间碎片。

调优:开启CMS压缩,查看参数是否合理。

// 开启CMS压缩,在FGC时执行压缩,默认为true

-XX:+UseCMSCompactAtFullCollection

// 执行几次FGC才执行压缩,默认为0

-XX:CMSFullGCsBeforeCompaction=0

2)并发清理可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生

调优:可能是触发GC的比例太高,适当调低该值。

// CMS触发GC的比例

-XX:+UseCMSInitiatingOccupancyOnly

-XX:+CMSInitiatingOccupancyFraction=70

3)对CPU资源非常敏感。在并发阶段,会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4。

调优:可能是并发线程数设置太高,适当调低该值。

// CMS并发线程数

-XX:ConcGCThreads=X

以上的调优只是针对一些可能性较大的问题给的建议,具体还是需要结合场景和完整的JVM参数去分析,各个参数可能都会影响到整体的GC效率。

5、Final Remark 阶段为什么还需要遍历 GCRoots?

这是因为 CMS 的写屏障(write barrier)并不是对所有会导致引用变化的字节码生效,例如不支持 astore_X(把栈顶的值存到本地变量表)。

至于为什么不为 astore_X 添加写屏障,R 大认为是栈和年轻代属于数据快速变化的区域,对于这些区域使用写屏障的收益比较差。

6、Final Remark 阶段还需要遍历 GC Roots,那之前的标记工作不是白做了?

不是的。

在三色标记法中(见下面介绍),如果扫描到被标记为黑色的对象就会终止,而之前的并发标记和预处理已经完成了绝大部分对象的标记,也就是此时大部分对象已经是黑色了,因此 Final Remark 阶段的工作其实会减少很多。简单来说就是:遍历的广度不变,但是深度变浅了。

7、三色标记算法?

三色标记算法由 Edsger W. Dijkstra 等人在1978年提出,是一种增量式垃圾回收算法,增量式的意思是慢慢发生变化的意思,也就是 GC 和 mutator(应用程序)一点点交替运行的手法。

与其相反的则是停止型GC,也就是GC时,mutator 完全停止,GC结束再恢复运行。

三色标记算法顾名思义就是将 GC 中的对象分为三种颜色,这三种颜色和所包含的意思如下:

白色:还未搜索过的对象。在回收周期的开始阶段,所有对象都为白色,而在回收周期结束时,所有白色对象均为不可达对象,也就是要回收的对象。

灰色:正在搜索的对象。已经被搜索过的对象,但是该对象引用的对象还未被全部搜索完毕。

黑色:搜索完成的对象。本身及其引用的所有对象都被搜索过,黑色对象不会指向白色对象,同时黑色对象不会被重新搜索,除非颜色发生变化。

我们以 GC 标记-清除算法为例简单的说明一下。

GC 开始运行前所有的对象都是白色。GC 一开始运行,所有从根能到达的对象都会被标记为灰色,然后被放到栈里。GC 只是发现了这样的对象,但还没有搜索完它们,所以这些对象就成了灰色对象。

灰色对象会被依次从栈中取出,其子对象也会被涂成灰色。当其所有的子对象都被涂成灰色时,该对象就会被涂成黑色。当 GC 结束时已经不存在灰色对象了,活动对象全部为黑色,垃圾则为白色。

下面是一个三色标记算法的示例动图,大家参考着理解。

明白了三色标记算法后,再回过头去看第5题,是不是顿时就明白了。

8、三色标记算法存在的问题?

三色标记算法是增量式垃圾回收算法,mutator可能会随时改变对象引用关系,因此在并发下会存在漏标和错标(多标)。

1)漏标

直接通过一个简单的例子来看:

假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1和2,也就是到了时刻3的场景,GC线程继续执行。

此时对象Z只被黑色对象X所引用,而黑色对象是不会被继续扫描的,因此扫描结束后Z仍然是白色对象,也就是时刻4,此时白色对象Z则会被当做垃圾而回收。

2)错标(多标)

直接通过一个简单的例子来看:

假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1,也就是到了时刻2的场景,GC线程继续执行。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

[外链图片转存中…(img-z7RjZic3-1715824462242)]

[外链图片转存中…(img-sjqdgbJH-1715824462242)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值