标记垃圾,有三种色彩:四千长文带你深入了解三色标记算法

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者

📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代

🌲文章所在专栏:JVM

🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识

💬 向我询问任何您想要的东西,ID:vnjohn

🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏

😄 代词: vnjohn

⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • 可达性分析算法
  • 标记过程
  • 三色标记算法
    • 多标
    • 漏标
    • Compare
    • 总结

      前言

      回顾 优化内存利用:深入了解垃圾回收算法与回收器 一文,介绍了垃圾回收算法、垃圾收集器(垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的实践者)

      主要谈到了 CMS、G1 这两种垃圾收集器,它们都有一个共同的特征,可支持并发标记,从此文来介绍它们两者在并发标记阶段共同所使用的算法

      可达性分析算法

      当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活的,可达性分析算法理论上要求全过程都基于一个能保障一致性的快照才能进行分析,这意味着必须全程冻结用户线程的运行

      在 JVM 整个内存结构布局中,堆的大小占比是特别大的,堆越大,存储的对象就越多,对象图结构就越复杂,从而就要标记更多的对象而产生的 STW 时间就会越长

      “标记阶段”是所有垃圾收集算法的共同特征,若这个阶段随着堆变大而增加停顿时间,其所有的垃圾收集器都会在这个阶段都会波及到一定影响,若能够削减这部分带来的停顿时间,收益也将会是系统性的提高

      标记过程

      简要回顾一下 CMS、G1 两者垃圾收集器在进行垃圾回收时所要发生的四个阶段:

      • CMS:初始标记、并发标记、重新标记、并发清除
      • G1:初始标记、并发标记、最终标记、筛选回收

        CMS、G1 两者都有并发标记这个阶段,导致了它们两者在用户线程、GC 回收线程同时运行时负载会有所不同,同时也会出现一些引用标记的问题「多标、漏标」

        三色标记算法

        若要解决或降低用户线程的停顿,首先就会搞清楚为什么必须在一个能保障一致性的快照上同时还能进行对象图的遍历操作

        能保障一致性的快照上同时还能进行对象图的遍历操作:指的就是并发标记阶段

        在垃圾收集器中实践阶段,引入了 “三色标记” 算法作为辅助工具,把遍历对象图过程中遇到的对象,按照 “是否访问过” 这个条件来标记成三种不同颜色的引用对象

        1. 白色:表示对象尚未被垃圾收集器访问过,在根可达分析刚开始的阶段,所有对象都是白色的,若在标记阶段结束后,仍然是白色的对象,即代表不可达,对象就会被回收 > 处于消亡状态
        2. 黑色:表示对象以及被垃圾收集器访问过,且这个对象关联的所有引用都已经被扫描过;黑色的对象代表已经被扫描过,它是安全存活的,若其他对象引用指向了黑色对象,无须再重新扫描一遍
        3. 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用(属性对象)还没被扫描过

        初始状态,只有 GC Roots 是黑色的

        标记过程:并发垃圾收集的标记阶段过程中,灰色对象的标记状态不断向前推进,从黑色对象(已完成标记)扩展到白色对象(尚未被访问)灰色对象是黑、白对象之间的分水岭

        从根对象逐步向下扫描,相当于就是在对象图上从黑向白推进「灰色对象作为黑、白两者之间的分水岭」过程,从 Serial 系列、Parallel 系列垃圾收集器来看,当出现垃圾收集时,它们的用户线程是处于冻结态的,只有垃圾收集线程是处于工作态的,就不会出现对象图很乱的问题

        故而言之,Serial 系列、Parallel 系列收集器无须额外使用三色标记算法去处理,它们采用追踪式垃圾收集算法(标记-复制、标记-清除、标记-整理)处理即可

        标记完成:标记阶段完成,此时黑色对象就是存活的对象,白色对象就是已消亡且可回收对象

        可是,当使用了 CMS 或 G1 垃圾收集器(并发垃圾收集器,支持垃圾收集线程、用户线程并发执行)时,这时候情况就不一样了,垃圾收集线程会在对象图结构上进行颜色标记,同时用户线程也在修改引用关系(即修改对象图的结构)这时就会出现两种后果

        多标、漏标

        多标

        多标:将原来消亡的对象错误标记为存活,这不是好事,增加了额外的内存空间浪费,但其实是可以容忍的,只不过产生了一些逃过本次垃圾收集产生的浮动垃圾而已,下次再进行清理即可

        在颜色状态推进的过程中,正在扫描的黑对象引用切断与灰色对象关系(此时应已被标记为消亡态),但此时又有另外一个黑色对象将其标记了(此时又由最初的消亡态变换为存活态)

        漏标

        漏标:将原来存活的对象错误标记为已消亡,这种情况下就会比较严重,程序会因此发生程序错误,例如:Class Xxx Not Found

        当且仅有两个条件同时满足时,会造成漏标问题的产生,即原本应当是黑色对象的被误标记为白色对象

        1. 插入了一条或多条从黑色对象到白色对象的新引用
        2. 删除了所有从灰色对象到白色对象的新引用

        因此,要解决在并发标记阶段所造成的漏标问题,只需要破坏这两个条件中的任意一个条件即可,分别产生了以下两种解决方案:增量更新(Increment Update)以及原始快照(Snapshot At The Beginning -> SATB)

        我们来看造成漏标问题的第一个条件是如何产生的,如下图:

        结合第一张、第二张图来看,再看上图,在颜色状态推进的过程中,正在扫描的灰色对象引用切断与白色对象的联系,同时原来白色引用的对象又已经跟扫描过的黑对象建立了引用关系

        增量更新的方案是破坏了第一个条件,当黑色对象插入新的指向白色对象关系时,就将这个新插入的引用记录下来,等待并发标记阶段结束以后的下一个阶段,再将这些新插入的引用记录,以黑色对象为根,重新扫描一次

        CMS 采用的就是增量更新的方式来处理漏标问题,在它的重新标记阶段进行处理,可以简单理解为,一旦黑色对象插入了新的指向白色对象的引用,它就会变为灰色对象

        我们来看造成漏标问题的第二个条件是如何产生的,如下图:

        被切断后的对象重新被黑色对象所引用的对象可能是原有引用链中的一部分,由于黑色对象只会扫描一次,这将导致扫描结束后出现两个被黑色对象所引用的对象仍是白色,所以这两个白色对象就会消失,这种情况就很严重了

        原始快照的方案就是破坏了第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发标记阶段后的下一个阶段,再将这些删除的引用关系以灰色对象为根,重新进行一次扫描工作

        G1 采用的就是原始快照 SATB 方式来处理漏标问题的,在它的最终标记阶段进行处理,也可以简单理解为,无论引用关系删除与否,都会按照刚开始扫描的那一刻对象图快照记录来进行重新扫描

        Compare

        增量更新、原始快照方式,无论是对引用关系的插入还是删除,它们的记录操作都是通过写屏障技术来完成的

        写屏障技术被用于记录对象的标记状态,写屏障技术一旦有引用关系发生了变化,它都会进行记录,但现有的 CMS、G1 都采用了插入式写屏障技术来进行优化,减少了一些性能上的开销工作

        考虑性能的高低以及两者之间的权衡来决定以黑为根还是以灰为根来进行一次重新扫描工作

        增量更新:会重新以黑色对象为根进行重新扫描(黑色—>白色),会浪费多一些时间,但考虑到发生漏标问题的情况也不太常见,所以扫描这部分黑色对象自然也就不多

        原始快照:可能会把原本要取消引用的对象(灰色—>白色)给错误的标记为存活状态了,从而会产生一些浮动垃圾,也就是前面所说到的多标问题,能够被忽略

        总结

        该篇博文主要介绍了 CMS、G1 在「并发标记」阶段共同使用到的一种算法:三色标记算法,简要说明了它的多标问题,重点介绍了在使用其算法时会发生的漏标问题,有两种方式可以用来解决这种问题:增量更新、原始快照,CMS 使用的是前者,G1 使用的后者,最后对这两种不同解决方案方式作了一下对比,希望此博文你能够喜欢!

        参考文献:《深入理解 Java 虚拟机》周志明著

        博文放在 JVM 专栏里,欢迎订阅,会持续更新!

        如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

        推荐专栏:Spring、MySQL,订阅一波不再迷路

        大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!