JVM 性能分析—— 一文带你读懂 CMS 垃圾收集器收集流程

概述

在详细讲解 CMS 与 G1 垃圾收集器之前,先介绍其中的一些知识点,方便后续理解。

跨代引用

跨代引用问题

跨代引用指的是一个老年代对象引用了一个年轻代对象。这种引用关系跨越了代的边界,称为跨代引用。

假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,除了遍历 GC Roots 之外, 还要额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。(通常能单独发生收集行为的只是新生代,所以这里“反过来”的情况只是理论上允许,实际上除了 CMS 收集器,其他都不存在只针对老年代的收集。所以年轻代引用老年代不算在跨代引用范围内)。

跨代引用相对于同代引用来说仅占极少数,不应再为了少量的跨代引用去扫描整个老年代。如果不加处理,在进行垃圾收集时,需要扫描整个老年代以来查找垃圾对象,这可能会导致较长的停顿时间。为了解决这些问题,分代垃圾收集器通常会采用卡表(Card Table)机制来跟踪跨代引用。把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生 Minor GC 时,只有包含了跨代引用的小块内存里的对象才会被加入到 GC Roots 进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值) 时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

记忆集(Remember Set)和卡表(Card Table)

使用记忆集来缩减 GC Roots 扫描范围的问题

记忆集(Remember Set):是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

  • 记忆集是老年代对象持有的指向年轻代对象的引用的集合。
  • 它用于记录老年代对象到年轻代对象的跨代引用关系。
  • 当年轻代发生垃圾收集时,收集器只需扫描记忆集,而不需要扫描整个老年代。这大大提高了年轻代收集的效率。

卡表(Card Table)是记忆集的一种具体实现,卡表将堆内存划分为一个个特定大小的内存块,被称作 “卡页”(Card Page),一个卡页的内存中通常包含不止一个对象, 只要卡页内有一个(或更多)对象的字段存在着跨代引用变化, 那就将对应卡页标识为1,称为这个卡页变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入 GC Roots 中一并扫描。

  • 卡表是一种辅助数据结构,用于记录老年代对象引用关系的变化。
  • 它将老年代的内存空间划分为多个固定大小的 “卡页”(Card Page)。
  • 当老年代中的对象发生引用关系的变化时,虚拟机会将相应的 “卡页” 标记为"脏"(dirty)。
  • 在年轻代垃圾收集时,收集器只需扫描那些被标记为"脏"的卡,就可以找到跨代引用,而不需要扫描整个老年代。

写屏障

解决如何维护卡表状态的问题,例如它们何时变脏、谁来把它们变脏等

变脏时间点原则上应该发生在引用类型字段赋值的那一刻。写屏障可以看作在虚拟机层面对 “引用类型字段赋值” 这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作。

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与 Minor GC 时扫描整个老年代的代价相比还是低得多的。

三色标记算法

用户线程与收集器是并发工作时,解决线程切回到收集器进行回收时从哪些对象开始遍历标记的问题

三色标记算法可以高效地完成可达性分析,减少不必要的对象扫描,提高垃圾收集的性能。

在进行遍历标记对象图过程中,按照 “是否访问过” 这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

增量收集

解决全量收集 “Stop-The-World” 长停顿时间,可以大大缩短应用程序的停顿时间

在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 Stop the World 状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集算法的诞生。

增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对进程间冲突的妥善处理,允许垃圾收集进程以分阶段的方式完成标记、清理或复制工作。

增量收集过程中(并发标记阶段)可能出现两种后果:

  • 一种是把原本消亡的对象错误标记为存活,这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。

  • 另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误。当且仅当以下两个条件同时满足时,会产生 “对象消失” 的漏标问题,即原本应该是黑色的对象被误标为白色:

    1. 已被遍历标记过的黑色对象重新引用了该白色对象;
    2. 删除了全部从灰色对象到该白色对象的直接或间接引用。

    在这里插入图片描述

增量收集过程中(并发标记阶段)漏标问题解决:

增量收集过程中(并发标记阶段)漏标问题,只需要破坏两个条件中的一个就行:

  1. 增量更新

    • 增量更新要破坏的是第一个条件。当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
    • CMS 是基于增量更新来做并发标记的。
  2. 原始快照

    • 原始快照要破坏的是第二个条件。当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
    • G1、Shenandoah 则是基于原始快照来做并发标记的。

CMS(Concurrent Mark-Sweep)

在这之前的其他垃圾收集器,如 Serial 收集器、ParNew 收集器、Parallel 收集器,停顿时间比较长,最多是使用多线程并行多个垃圾收集线程进行垃圾回收来优化减少收集时间。他们都不能并发运行(用户线程与垃圾收集线程并发运行),停顿时间长,与用户交互体验就会变差。而 CMS 就是第一款并发收集器,尽量减少 stw 的时间,提升用户的体验。CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。

启用参数:-XX:+UseConMarkSweepGC

CMS 是一个老年代垃圾收集器,默认结合 ParNew 收集器作为年轻代垃圾收集器。

目标: 尽量减少 stw 的时间,提升用户的体验。真正做到 GC 线程和用户线程几乎同时工作。CMS 采用标记-清除算法。

CMS 老年代垃圾清理(Major GC)流程

在这里插入图片描述

1、初始标记

  1. 首先,CMS 会暂停应用程序的工作线程,使它们进入停顿状态,即 “Stop-the-World" 机制(STW)。
  2. 然后,CMS 会从 GC Roots、新生代开始进行标记,标记直接可达(直接关联)的对象为灰色,如1、2、5对象被标记为灰色。这个过程比较快,因为只需要扫描少量的对象及直接关联对象。因为 CMS 是老年代垃圾收集器,新生代的对象引用老年代对象,依然属于老年代外部的引用,因此仍然有必要认为其引用链是有效的。
  3. 标记完成后,CMS 会恢复应用程序的工作线程,让它们继续执行。
    在这里插入图片描述

2、并发标记

从GC Roots 的直接关联对象(1、2、5)开始遍历整个引用链的过程,这个过程耗时较长但是不需要 STW,用户线程与垃圾收集线程一起并发运行。这个过程中,可能会有导致已经标记过的对象状态发生改变。

从 1、2、5 开始遍历对象,遍历完对象及其引用后,这些对象会变为黑色,如1、2、5、4。因为是并发标记,用户线程与垃圾收集线程切换运行,当切换回垃圾收集线程继续标记时,会从灰色对象开始遍历,不需要从头开始遍历已遍历的黑色对象(三色标记起到作用了),这里没再画出这个中间流程了。

其次,并发标记可能有新对象晋升到老年代,如对象 7,晋升后的 7 没有被引用,可以先不处理,待后续重新标记判断是否有被引用;

还有可能引用变更,如对象 8,新增了对象 2 对对象 8 的引用,但对象 2 已经变为黑色了,理论下次不会再从对象 2 开始遍历,这样会导致对象 8 不会被遍历到当做垃圾被清理掉,这肯定是不行的。于是就会使用到前面介绍的卡表(Card Table),对于引用更新,会将对应的卡页(Card Page)标记为脏(Dirty Page),如对象 2 所在的卡页被标记为脏,后续预清理(本文不介绍)和重新标记阶段就对 Dirty Page 中的所有对象都重新进行一遍引用分析。
在这里插入图片描述

3、重新标记

为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。

重新标记阶段需要进行第二次 STW。这个阶段会从 GC Roots、新生代、Dirty Card 重新标记。标记为黑色的对象不会再进行遍历(尽管黑色对象可能又没有被引用),因为很多对象在并发标记阶段都已标记为黑色,不会重新遍历对应的整个引用链,所以这个阶段的通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。在 STW 下进行最后的修正标记出老年代所有的存活对象,如对象 10、11、8 遍历标记为黑色。最后剩下对象 3、7 未遍历到为白色,会被当做垃圾处理。
在这里插入图片描述

4、并发清理

经过前面的标记处理,老年代的所有存活对象都已经被标记出来,此阶段对不可达对象(白色对象)进行回收,释放堆内存空间。这个阶段是用户线程与垃圾收集线程并发运行,不存在 STW,如果有新增对象出现会被标记为黑色不做任何处理。

在 CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行时自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此 CMS 收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。

5、并发重置

重置相关数据结构(本次 GC 过程中的标记数据)的一种机制。用户线程与垃圾收集线程并发运行,不存在 STW。

CMS 垃圾收集器缺点

1、CPU 资源消耗高

  • CMS 收集器对处理器资源非常敏感。CMS默认启动的回收线程数是 (处理器核心数量+3)/4, 也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS 对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。到 JDK 9 发布后 CMS 模式被完全废弃。

2、浮动垃圾

  • 由于 CMS 收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现 “Con-current Mode Failure” 失败进而导致另一次完全 “Stop The World” 的 Full GC 的产生。CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此 CMS 收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
    (Minor GC、Full GC 都是独占式收集,用户线程暂停(STW),Major GC 是并发收集,用户线程和垃圾收集线程并发运行)
  • CMS 收集器当老年代使用了 68% 的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快, 可以适当调高参数 -XX:CMSInitiatingOccu-pancyFraction 的值来提高 CMS 的触发百分比,降低内存回收频率,获取更好的性能。到了 JDK 6 时,CMS 收集器的启动阈值就已经默认提升至 92%。但这又会更容易面临另一种风险:要是 CMS 运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参
    数 -XX:CMSInitiatingOccupancyFraction 设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。

3、内存碎片

  • CMS是一款基于 “标记-清除” 算法实现的收集器,意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
  • 为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMS-CompactAtFullCollection 开关参数,用于在 CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前) 是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数 -XX:CMSFullGCsBefore-Compaction,这个参数的作用是要求 CMS 收集器在执行过若干次(数量由参数值决定)不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CMS(Concurrent Mark Sweep)是一种并发的垃圾回收器,它的主要优势是尽可能地减少应用程序的停顿时间。下面是CMS垃圾回收器的实现原理: 1. 初始标记(Initial Mark):这个阶段会暂停应用程序的线程,标记出所有的根对象,即 GC Roots,比如静态变量和活动线程等。这个阶段很快就完成了,但是应用程序会被短暂地暂停。 2. 并发标记(Concurrent Mark):在这个阶段,垃圾回收器会扫描堆中的对象,标记出所有活跃的对象,这个阶段与应用程序是并发执行的,所以不会停顿程序执行。因为这个阶段与应用程序并发执行,所以在这个阶段,堆中的对象可能会发生变化,因此需要在下一个阶段扫描时重新确认它们是否仍然是活跃的。 3. 重新标记(Remark):在这个阶段,垃圾回收器会重新遍历堆中的对象,标记出在并发标记阶段发生变化的对象。这个阶段需要暂停应用程序,但是它的时间通常比初始标记阶段更短。 4. 并发清除(Concurrent Sweep):在这个阶段,垃圾回收器会扫描堆中的对象,清除所有被标记为垃圾的对象。这个阶段与应用程序是并发执行的,因此不会停顿程序执行。由于在清除对象时不需要移动对象,因此这个阶段的性能通常比标记阶段更高。 需要注意的是,CMS垃圾回收器并不会移动对象,因此当堆中的对象达到一定的程度时,会出现碎片化的问题。为了解决这个问题,JVM还提供了G1(Garbage First)垃圾回收器。 另外,CMS垃圾回收器虽然能够减少应用程序的停顿时间,但是在执行过程中会产生一些额外的负载,因此需要根据应用程序的特点和需求来选择合适的垃圾回收器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值