深入理解JVM02.1 垃圾回收CMS算法

140 阅读12分钟

本文章的内容来自以下的内容
【JVM调优实战】对比:golang与java的GC(全网首讲)_哔哩哔哩_bilibili
《深入理解Java虚拟机- -jvm高级特性与最佳实践》

JDK 8 和11

JDK 8的默认垃圾收集器配置是新生代使用Parallel Scavenge收集器。
JDK 11 开始,G1 成为了默认的垃圾收集器,引入了新的垃圾收集器 ZGC。

CMS 和 Parallel Scavenge算法

Concurrent Mark Sweep,Concurrent指的是业务线程和垃圾回收线程并发。
CMS算法的主要目标:减少垃圾收集过程中应用程序的STW停顿时间(也称为"Stop-the-World"时间)

image.png

四个阶段

  • 初始标记
    • STW阶段,时间特别短
    • 标记直接与GC Roots相连(直接引用)的对象。
    • 对存活的对象进行初步标记,并将这些对象的颜色标记为黑色
  • 并发标记
    • 用户线程可以继续运行,而垃圾收集器则继续标记对象
    • 三色标记算法(Golong也使用该算法)
  • 重新标记
    • STW阶段,时间特别短
    • 黑色对象保持不变,灰色对象变为黑色,而白色对象变为灰色
    • 清理标记错误的垃圾
    • 1,原来不是垃圾,变成了垃圾
    • 2,原来是垃圾,被重新启用
  • 并发清理
    • 垃圾收集器并发地清除所有未被标记的对象
    • 使用标记-清除方法

GC Root直接引用和不能直接引用

上面我们讲到,在开始阶段,在GC Roots开始标记所有直接可达的对象,但是直接可达/引用和不直接引用的区别是什么呢?详解可以看一下这两篇文章

GC Roots 是什么?哪些对象可以作为 GC Root?看完秒懂!-CSDN博客
一篇文章彻底搞懂GC - 个人文章 - SegmentFault 思否
一篇文章彻底搞懂GCJava相较于其他编程语言更加容易学习

一些具体的例子

注意:这只是常见的一些例子,不包含所有情况。

类别GC Roots能够直接引用的项目GC Roots不能直接引用的项目
栈中引用当前线程的栈帧中局部变量表里的对象引用其他线程的栈帧中局部变量表里的对象引用(因为GC Roots基于当前线程和当前栈帧)
静态字段类的静态变量所引用的对象类的实例变量(非静态字段)所引用的对象(除非这些实例变量是GC Roots本身的一部分,如系统类加载器的Class对象)
JNI/本地方法栈JNI(Java Native Interface)或本地方法栈中通过本地代码持有的Java对象引用JNI/本地方法栈中未通过当前JNI调用持有的Java对象引用
活跃线程活跃线程对象及其线程局部变量(如果JVM实现支持)非活跃线程对象及其相关引用(这些对象在线程结束后可能成为垃圾收集的目标)
系统类系统类加载器加载的Class对象及其静态成员应用程序自定义类加载器加载的Class对象及其静态成员(除非这些类加载器本身或它们的Class对象被GC Roots引用)

三色标记算法详解

如果有不懂得地方,这篇文章讲的很清楚,接下来的内存也引用自这篇文章:
图文详解 三色标记算法_三色标记法详细图解-CSDN博客

三色具体是什么

  • 白色:没有检查(或者检查过了,确实没有引用指向它了)
  • 灰色:自身被检查了,成员没被检查完(可以认为访问到了,但是正在被检查,就是图的遍历里那些在队列中的节点)
  • 黑色:自身和成员(子集,叶子)都被检查完了

三色标记算法的具体过程

假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:

  • 初始时,所有对象都在 【白色集合】中;
  • 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
  • 从灰色集合中获取对象:
    • 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
    • 将本对象 挪到 【黑色集合】里面。
  • 重复步骤3,直至【灰色集合】为空时结束。
  • 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。
  • 以上过程其实就是数的遍历

三色标记算法的问题

文章引用自:面试整理:三色标记法、写屏障、增量更新和原始快照-CSDN博客
如果想详细了解G1和ZGC可以看看这个

第一种问题: 错标

 标记过不是垃圾的,变成了垃圾(也叫浮动垃圾),如下图:标记完了E是D的一个引用,也就是说此时E是灰色的,但是D断开的对E的引用。这个浮动垃圾的问题影响不是很大,可能就是暂时的浪费一点内存,它肯定抗不过下一轮GC。

image.png

错标的解决办法

写屏障

只要在A写B的时候加入写屏障,记录下B被切断的记录,重新标记时可以再把他们标为白色即可。

第二种问题:漏标,或者叫错杀

这个问题是比较致命的,如果错杀了,就会出现运行结果不符合预期的情况。这个是绝对不能发生的。这发生的情况只有一个,就是D是黑色的,E是灰色的,但是D又指向了G,和E断开了指向G。 因为D已经标记了是黑色,但是E断开了引用,所以G就当做了是白色的。这个时候如果不操作的话,就会把G错杀掉。这种问题是必须解决掉的。 image.png

错杀的解决办法

注意:D想要指向G,这是一个写操作
错标只有在满足下面两种情况下才会发生:

  1. 灰色指向白色的引用全部断开。
  2. 黑色指向白色的引用被建立。
    只要打破任一条件,就可以解决错标的问题。

原始快照和增量更新
原始快照(也称为屏幕快照)打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。相当于无论引用关系是否删除,都会按照刚开始扫描时那一瞬间的对象图快照来扫描。

增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。

G1是打破了第一个条件:写屏障和屏幕快照。
CMS打破的是第二个条件:写屏障+增量更新。

当黑色指向白色的引用被建立时,通过写屏障来记录引用关系,等扫描结束后,再以引用关系里的黑色对象为根重新扫描一次即可。

增量更新,原始快照(SATB)、写屏障、读屏障的具体实现过程和代码

原始快照(SATB)

当灰色对象(正在被GC扫描的对象)断开对白色对象的引用时,SATB会记录这个断开的引用,使得GC在最终标记阶段可以按照扫描开始时的对象图快照来扫描对象,即使对象间的引用关系在并发标记过程中发生了变化。

增量更新

黑色对象(已被GC扫描且其所有引用也被扫描的对象)建立对白色对象(还未被GC扫描的对象)的新引用时,CMS会将这个黑色对象重新标记为灰色(表示该对象还需要被进一步扫描其引用),以便在并发标记阶段结束后重新扫描这些灰色对象,从而确保所有应被标记的对象都被正确标记。

写屏障

  1. 触发时机:当某个线程尝试更新一个对象的字段(即写入新的对象引用)时,写屏障被触发。

  2. 执行操作

    • 记录或更新信息:写屏障可能会记录旧引用和新引用之间的某种关系,或者更新某些数据结构(如标记队列、卡片表等),以便GC能够知道这个引用关系的变化。
    • 颜色传播:在某些实现中,如果旧引用是灰色(正在被GC扫描)而新引用是白色(尚未被GC扫描),写屏障可能会将新引用也标记为灰色,或者将旧引用和新引用都添加到待重新扫描的列表中。
    • SATB(Snapshot At The Beginning) :在SATB算法中,写屏障用于记录灰色对象断开的白色对象引用,以便GC可以按照扫描开始时的对象图快照进行标记。
  3. 继续执行:完成屏障操作后,原始的对象引用更新操作继续执行。

读屏障

  1. 触发时机:当某个线程尝试读取一个对象的字段(即加载对象引用)时,读屏障被触发。

  2. 执行操作

    • 标记对象:读屏障可能会将读取的对象标记为灰色(或等效状态),表示该对象需要被GC进一步处理。这有助于确保即使在并发环境下,GC也能够正确地跟踪和标记所有可达对象。
    • 记录依赖:在某些实现中,读屏障还可能记录当前线程或读取操作与读取对象之间的某种依赖关系,以便在后续阶段进行更精细化的处理。
  3. 返回引用:完成屏障操作后,读屏障将读取的对象引用返回给原始操作,以便程序继续执行

CMS ,G1 ,ZGC的关系

  • CMS:写屏障 + 增量更新
  • G1:写屏障 + SATB(原始快照)
  • ZGC:读屏障

CMS

从未作为默认GC,JDK 8的默认垃圾收集器配置是新生代使用Parallel Scavenge收集器,

G1

JDK 9及之后的版本中作为默认垃圾收集器使用。

G1垃圾收集器的特点

  1. 逻辑分代,物理非分代:G1保留了分代的概念(新生代、老年代),但在物理上并不强制划分连续的内存区域。它使用多个大小可配置的Region来组成整个堆空间,每个Region可以根据需要扮演新生代或老年代的角色。
  2. 并发和并行:G1是并发并行的垃圾收集器,允许用户线程和GC线程同时工作,从而减少停顿时间并提高吞吐量。
  3. 可预测的停顿时间:G1允许用户设置期望的最大GC停顿时间(通过-XX:MaxGCPauseMillis参数),并尽力在不超过该时间的情况下完成垃圾收集。
  4. 面向Region的回收:G1不再局限于整个新生代或老年代的回收,而是可以基于Region的回收价值来选择回收哪些Region,这有助于减少内存碎片并提高内存使用效率。
  5. 动态调整:G1会根据应用的实际运行情况和用户设置的目标停顿时间,动态调整新生代和老年代的大小、晋升年龄等参数,以达到最优的GC性能。

G1的工作流程

G1的GC周期大致可以分为以下几个阶段:

  1. 新生代GC

    • 当Eden区内存耗尽时,触发新生代GC。
    • 回收Eden区和Survivor区中的垃圾对象,清空Eden区。
    • Survivor区中的对象可能会晋升到老年代,或者在同一Survivor区之间移动。
    • 新生代的大小可能会根据GC情况进行动态调整。
  2. 并发标记周期

    • 初始标记(STW):标记GC Roots直接关联的对象。
    • 根区域扫描:扫描由Survivor区直接可达的老年代区域,并标记这些对象。
    • 并发标记:并发扫描整个堆,查找并标记存活的对象。
    • 重新标记(STW):修正并发标记期间因用户线程活动而改变的对象引用。
    • 独占清理(STW):计算每个Region的回收价值,对Region进行排序,识别可供混合回收的区域。
    • 并发清理:识别并清理完全空闲的Region。
  3. 混合回收

    • 在并发标记周期结束后,G1会基于Region的回收价值进行混合回收。
    • 优先回收垃圾最多的Region,这些Region可能包含新生代和老年代的对象。
    • 被清理Region中的存活对象会被移动到其他Region,以减少内存碎片。
  4. Full GC

    • 如果G1的回收速度跟不上用户线程的内存分配速度,或者在某些极端情况下,G1会触发Full GC来获取更多的可用内存。
    • Full GC是G1的备用方案,通常意味着G1的默认行为无法满足当前的性能需求。

使用G1的参数

  • -XX:+UseG1GC:开启G1垃圾收集器。
  • -XX:MaxGCPauseMillis:设置GC停顿时间的目标值。
  • -XX:ParallelGCThreads:设置并行GC的线程数。
  • -XX:InitiatingHeapOccupancyPercent:设置触发并发标记周期的堆占用率阈值。

通过合理配置这些参数,可以使G1更好地适应应用程序的需求,提高GC性能和应用程序的响应能力。

ZGC

JDK11引入了ZGC