【Java】GC

三色标记

标记法在垃圾回收中确实至少需要三种颜色才能安全地支持并发标记。这主要是为了在垃圾回收器与程序同时运行时,高效且正确地区分对象状态。

颜色在垃圾回收中的含义关键作用
白色对象尚未被垃圾回收器访问过,或垃圾回收结束时确认为可回收的垃圾。表示“未扫描”或“可回收”
灰色对象本身已被垃圾回收器访问,但其引用的其他对象尚未完全扫描。核心中间状态,表示“扫描中”,存在于待处理队列
黑色对象已被垃圾回收器访问,且其所有引用的对象也都已被扫描。表示“已扫描完毕”,在本次收集中存活

🔍 理解三色的必要性

三色标记法的精妙之处,特别是灰色这个中间状态的设计,主要体现在支持并发垃圾回收时能保持正确性。

  • 单线程环境两色足够:在停止所有应用线程(STW)进行垃圾回收的场景下,由于对象图不会变化,确实只需要两种状态(例如“已扫描”和“未扫描”)就能完成标记。回收器从根节点出发,遇到对象就标记为“已扫描”并继续遍历,所有遍历不到的就是垃圾。
  • 并发环境下三色必不可少:当垃圾回收器线程与应用线程并发执行时,对象间的引用关系会动态变化。此时,灰色状态就起到了关键的缓冲和协调作用。它标识出那些已经被垃圾回收器发现但其引用的对象还未被扫描的对象。垃圾回收器会按照一定顺序(如通过工作队列)处理这些灰色对象。如果没有灰色这个明确的中间状态,垃圾回收器在并发过程中就难以可靠地跟踪哪些对象需要继续扫描,从而无法保证标记的完整性,极易出现漏标(即存活对象被错误回收)等严重问题。

💡 三色标记如何工作及应对挑战

在并发标记过程中,三色标记法主要面临两个问题:浮动垃圾(多标)和对象漏标

  • 标记流程:垃圾回收器首先将所有对象标记为白色,从根对象(如全局变量、栈上的引用等)开始,将其标记为灰色并放入待处理集合。然后,垃圾回收器从灰色集合中取出对象,将其标记为黑色,并将其直接引用的白色对象标记为灰色。重复此过程,直到没有灰色对象。最终,剩余的白色对象即为可回收的垃圾。

  • 浮动垃圾:在并发标记过程中,一些已经被标记为存活(黑色或灰色)的对象可能随后不再被使用,但这些对象在本轮收集中不会被回收,成为“浮动垃圾”。浮动垃圾通常不影响程序正确性,留待下一次垃圾回收时清理。

  • 对象漏标:这是必须解决的严重问题。当并发标记时,应用线程修改对象引用可能导致一个存活对象被漏标。漏标需要同时满足两个条件:

    1. 赋值器插入了一条或多条从黑色对象指向白色对象的新引用。

    2. 赋值器删除了所有从灰色对象到该白色对象的直接或间接引用。

      现代垃圾回收器(如HotSpot VM的CMS和G1)通过写屏障技术破坏这两个条件之一来解决漏标:

      • 增量更新:关注新引用的插入。当黑色对象引用了白色对象时,记录此关系,在重新标记阶段将这些黑色对象重新标记为灰色,使其被再次扫描。CMS收集器采用此方案。
      • 原始快照:关注旧引用的删除。在引用关系被切断前,记录旧的引用关系。在重新标记阶段,GC会认为这些被删除的引用仍然存在,并据此进行标记。G1收集器采用此方案。

💎 总结

总而言之,在支持并发的垃圾回收器中,三色标记法是必要的。其关键在于灰色这一中间状态,它使得垃圾回收器能够有效地组织和管理标记过程,明确哪些对象正在处理或待处理。写屏障技术则与三色标记法紧密配合,共同确保了在对象图动态变化的情况下标记的正确性,从而在减少应用程序停顿的同时,安全地完成垃圾回收。

希望这些解释能帮助您理解三色标记法中三种颜色的重要性。如果您对特定垃圾回收器如G1或ZGC的细节有进一步兴趣,我们可以继续探讨。

三色标记法

三色标记法是一种用于垃圾回收的核心算法,它通过模拟对象遍历过程,高效地区分出存活对象和垃圾对象。下面我们深入了解一下它的工作原理、面临的挑战以及实际应用。

🎨 核心概念与标记过程

三色标记法通过白色(White)灰色(Gray)黑色(Black) 三种颜色来模拟垃圾回收器遍历对象图的过程。

  • 白色:表示对象尚未被垃圾收集器访问过。在可达性分析开始时,所有对象都是白色。在分析结束后,如果对象仍然是白色,则意味着它不可达,可以被回收
  • 灰色:表示对象已经被垃圾收集器访问过,但该对象至少还有一个引用的对象没有被扫描过。灰色对象是待扫描的对象,是白色和黑色之间的中间状态。
  • 黑色:表示对象已经被垃圾收集器访问过,并且这个对象的所有引用都已经被扫描完毕。黑色对象是安全存活的,垃圾回收器不会再次扫描它们。

其标记过程可以概括为以下步骤:

  1. 初始时,所有对象都在白色集合中。
  2. 从GC Roots(如栈帧中的局部变量、静态变量等)出发,将所有直接被GC Roots引用的对象从白色移动到灰色集合
  3. 从灰色集合中取出一个对象,将其标记为黑色,然后遍历它引用的所有子对象。如果子对象是白色的,则将其标记为灰色并放入灰色集合。
  4. 重复步骤3,直到灰色集合为空。此时,所有存活对象都被标记为黑色,而剩余的白色对象就是不可达的垃圾,可以在后续阶段被清除。

⚠️ 并发环境下的挑战与解决方案

上述标记过程在暂停所有应用线程(Stop-The-World)的情况下是简单且准确的。但为了降低停顿时间,现代垃圾回收器(如G1、CMS)希望在不暂停应用线程的情况下并发地进行标记。这时就会遇到一个核心挑战:在标记过程中,应用线程可能会修改对象之间的引用关系,从而导致两种主要问题。

1. 浮动垃圾(多标)

这种情况危害较小。指的是一个对象本来已经不被任何存活对象引用了(即已经是垃圾),但在垃圾回收器扫描过后,应用线程才断开对它的引用。由于垃圾回收器不会重新扫描黑色对象,这个本应被回收的对象就被错误地保留了下来。这种垃圾被称为“浮动垃圾”,通常留待下一次垃圾回收时清理。

2. 对象漏标

这是必须解决的严重问题。它指的是一个存活对象被错误地标记为垃圾并回收,可能导致程序崩溃。漏标的发生需要同时满足两个条件

  • 条件一:赋值器(即应用线程)插入了一条或多条从黑色对象指向白色对象的引用。
  • 条件二:赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

只要破坏这两个条件中的任意一个,就能防止漏标。主流垃圾回收器采用了两种解决方案:

  • 增量更新(Incremental Update)破坏条件一。当黑色对象插入了指向白色对象的新引用时,就将这个黑色对象记录下来,并重新标记为灰色。这样垃圾回收器后续会重新扫描这个对象,从而扫描到那个白色的对象。采用这种方案的典型回收器是 CMS
  • 原始快照(Snapshot At The Beginning, SATB)破坏条件二。当要删除灰色对象指向白色对象的引用时,就将这个要删除的引用关系记录下。垃圾回收器在后续标记时,会假定这个被删除的引用仍然存在,并以此为基础继续标记那个白色的对象(即基于标记开始时对象图的快照)。采用这种方案的典型回收器是 G1Shenandoah

这两种解决方案都依赖于写屏障(Write Barrier) 技术。写屏障可以理解为在对象引用赋值操作前后插入的一段钩子代码(类似AOP),用于记录引用关系的变化,为增量更新或原始快照提供数据基础。

🔧 在实际垃圾回收器中的应用

三色标记法是现代并发垃圾回收器的基石。

  • CMS回收器:在其并发标记阶段使用三色标记法,并采用增量更新方案来解决漏标问题。它注重降低停顿时间。
  • G1回收器:同样在并发标记阶段使用三色标记法,但采用原始快照方案。G1还结合了基于Region的内存布局和可预测的停顿时间模型,是一款面向服务端应用的垃圾回收器。
  • ZGC与Shenandoah:这些新一代低延迟垃圾回收器也以三色标记法为基础,并结合了更先进的技术(如ZGC的着色指针),力争将停顿时间控制在毫秒甚至亚毫秒级别。

💎 总结

三色标记法通过黑、灰、白三种颜色状态清晰地刻画了对象在垃圾回收过程中的状态变迁,是支持并发标记的关键算法,极大地减少了垃圾回收时的停顿时间。它通过写屏障技术,结合增量更新原始快照策略,有效解决了并发标记中最棘手的对象漏标问题,从而奠定了现代高性能垃圾回收器(如G1、ZGC)的基础。

希望以上解释能帮助你透彻地理解三色标记法。如果你对某个具体的垃圾回收器如何实现这些细节感兴趣,我们可以继续深入探讨。

GCRoots

在 Java 虚拟机中,GC Roots 是垃圾回收过程的起点,它们就像一组“锚点”,确保那些正在被使用的对象不会被错误地清理掉。下面这个表格能让你快速抓住核心要点。

GC Roots 类型具体示例生命周期与作用
虚拟机栈中的引用方法中的局部变量、参数等。方法调用期间有效,是 GC 最主要关注的 Roots。
方法区中静态属性引用的对象类的静态变量(static字段)。生命周期与类相同,只要类未被卸载,其引用的对象就会存活。
方法区中常量引用的对象字符串常量池(String Table)里的引用,static final常量。常量池中的引用会长期存在,类似于静态属性。
本地方法栈中JNI引用的对象在 Native 方法(通常由 C/C++ 实现)中创建或引用的 Java 对象。确保本地代码在操作 Java 对象时,该对象不会被回收。
活跃的线程对象正在运行的线程(Thread 实例),如主线程、子线程。线程在执行任务期间必须存活。
被同步锁持有的对象synchronized关键字锁住的对象。在同步块执行期间,锁对象必须存活以保证同步逻辑正确。
Java 虚拟机内部的引用系统类加载器、常用异常类(如 NullPointerException)、基本类型的 Class 对象等。JVM 运行所必需的核心对象,会常驻内存。

🔍 可达性分析与GC Roots的工作原理

GC Roots 之所以关键,在于它们是基于 可达性分析算法 的垃圾回收机制的基石。

  1. 作为起点:垃圾回收器会首先枚举出所有的 GC Roots 对象。
  2. 遍历对象图:从这些 GC Roots 出发,递归地遍历它们所引用的所有其他对象。这个遍历过程形成的路径被称为 引用链(Reference Chain)
  3. 判定对象生死:任何能够从 GC Roots 通过引用链到达的对象,都被判定为“存活”对象。反之,如果一个对象从 GC Roots 出发不可达,那么就意味着它已经不再被使用,可以被标记为垃圾并进行回收

这个机制的一个巨大优势是能够完美地解决 对象间循环引用 的问题。即使对象A引用对象B,对象B又引用对象A,但只要它们与GC Roots之间不存在任何引用链,它们依然会被正确地判定为可回收的垃圾。

💡 关键细节与深入理解

了解以下几点,可以帮助你更深入地理解 GC Roots:

  • 为什么实例字段不是GC Roots? 这是 JVM 的一种设计权衡。如果每个对象的每个实例字段都算作 GC Root,那么 GC Roots 集合将会无比庞大,严重拖慢垃圾回收的效率。实例字段的生命周期理应由其所属对象的可达性决定,这符合逻辑上的从属关系,也能避免因循环引用导致的内存泄漏。
  • GC Roots 的动态性:GC Roots 集合并非一成不变。例如,当一个方法执行完毕,其栈帧出栈,对应的局部变量引用就会从 GC Roots 中消失。同时,新的线程启动或新的方法被调用,又会加入新的 GC Roots。
  • “Stop-The-World”的必然性:为了进行准确的可达性分析,枚举 GC Roots 的过程必须在一個确保一致性的内存快照中进行。这意味着垃圾回收器在根节点枚举(以及后续标记过程的一些阶段)时必须暂停所有用户线程(Stop-The-World),否则引用关系可能在分析过程中发生变化,导致错误。
  • 分代收集下的 GC Roots:在进行部分GC(如只回收新生代的 Minor GC)时,除了全局的 GC Roots,老年代对象对新生代对象的引用也必须考虑。这些老年代对象会被临时性地加入 GC Roots 集合(称为 “Remembered Set” 机制的一部分),以确保分析的准确性。

🌱 总结

总而言之,GC Roots 是 JVM 垃圾回收机制的核心概念。它通过可达性分析,精准地判断对象的存亡。理解哪些对象可以作为 GC Roots,不仅有助于从原理层面掌握 JVM 的内存管理,对于实际开发中诊断内存泄漏、进行性能调优也至关重要。

希望这些解释能帮助你透彻地理解 GC Roots。如果你对与之相关的垃圾回收算法(如分代收集)或者具体如何排查内存泄漏感兴趣,我们可以继续深入探讨。

直接引用

CMS垃圾回收器的初始标记阶段之所以只标记GC Roots的直接引用对象,主要是为了最大限度地减少STW(Stop-The-World)停顿时间。而关于GC Roots本身的颜色,在CMS使用的三色标记算法抽象模型中,GC Roots通常被视为初始的黑色对象

为了帮助你快速理解CMS的标记阶段,这里有一个简要的对比:

阶段标记目标是否STW耗时GC Roots颜色状态
初始标记仅标记GC Roots直接关联的对象很短(通常<10ms)通常被视为黑色(已标记且完成扫描)
并发标记从直接关联对象开始,递归标记整个引用链最长作为遍历的起点,其状态在初始标记已确定

🔍 初始标记为何只标记直接引用

CMS的设计目标是低停顿。初始标记阶段需要"Stop the World"(停止所有应用线程),如果在这个阶段不仅要标记直接引用,还要递归标记整个引用链,那么STW的时间会变得很长,这就违背了CMS的初衷。

因此,CMS采用了分阶段标记的策略:

  • 初始标记:只完成最快、最必要的一部分——标记GC Roots直接关联的对象。这些对象数量相对较少,标记速度极快,因此造成的停顿非常短暂。
  • 并发标记:在初始标记的基础上,从那些已被标记的直接关联对象(此时是灰色对象)出发,在应用程序同时运行的情况下,并发地遍历整个对象图,标记所有存活对象。这是整个标记过程最耗时的部分,但由于是并发的,不需要STW。

简单来说,这是一种权衡:将一次长时间的STW,转化为一次极短的STW加上一个长时间的并发操作,从而显著提升应用的响应速度。

⚫️ GC Roots的颜色与三色标记

在三色标记算法中,颜色是抽象概念,用于追踪标记过程的进度:

  • 白色:表示对象尚未被垃圾回收器访问到(默认状态,可回收)。
  • 灰色:表示对象本身已被垃圾回收器访问到(标记为存活),但它引用的其他对象还没有被完全检查。
  • 黑色:表示对象本身已被标记为存活,并且它引用的所有对象也已经被检查完毕

基于这个定义:

  1. GC Roots是起点:标记过程从GC Roots开始。
  2. GC Roots是存活的:它们本身就是根集合,必然存活。
  3. 初始标记完成后:在初始标记阶段,GC Roots被访问,并且它们直接关联的对象也被识别出来(这些直接关联对象会被标记为灰色,等待在并发标记阶段进一步遍历)。对于GC Roots本身而言,在这个简单的模型里,当它们直接关联的对象被记录后,就可以认为对GC Roots的"检查"完成了。因此,GC Roots通常被视为黑色的

这使得GC Roots成为一个稳定的起点,并发标记阶段可以从灰色的直接关联对象开始,继续遍历对象图。

🔄 理解完整的CMS标记流程

结合CMS的完整标记阶段,可以更清晰地看到其协作关系:

  1. 初始标记 (STW):标记GC Roots直接关联的对象。GC Roots本身可视为黑色,其直接引用的对象被标记为灰色。停顿时间极短
  2. 并发标记:GC线程与用户线程并发执行。从灰色对象开始,继续遍历整个对象图,完成存活对象的标记。耗时最长,但无需停顿
  3. 重新标记 (STW):修正并发标记期间因用户线程运行而产生的引用变化(解决"漏标"问题)。停顿时间比初始标记长,但远短于并发标记

💎 简单总结

  • CMS在初始标记阶段只标记GC Roots的直接引用对象,核心目的是为了最大限度地缩短STW停顿时间,这是CMS追求低延迟的关键设计权衡。
  • 在三色标记的抽象模型中,GC Roots通常被视为黑色对象,因为它们作为标记的起点,本身是存活的,并且在初始标记阶段其直接引用已被处理(标记为灰色)。

希望这些解释能帮助你透彻地理解CMS的标记机制。如果你对CMS的其他阶段或者如何解决并发标记中的"漏标"问题感兴趣,我们可以继续探讨。

写屏障

写屏障技术是现代计算机系统中确保数据一致性和正确性的关键机制,尤其在垃圾回收和持久化存储领域发挥着核心作用。它是一种在特定操作(如内存写入或引用赋值)前后插入的屏障指令或代码钩子,用于维持某种不变性条件。

下表概述了写屏障在不同领域扮演的角色和核心思想。

领域核心目标关键实现思想
垃圾回收支持并发标记,避免"漏标"存活对象拦截对象引用赋值操作,记录变更或直接修正对象标记状态
操作系统/文件系统确保元数据写入存储设备的顺序和持久性,防止数据损坏在关键写入操作前后刷新易失性硬件缓存,强制数据落盘
底层并发编程保证多线程环境下内存操作的顺序性和可见性使用特定CPU指令,防止编译器和处理器对内存访问指令进行重排序

🧠 垃圾回收中的写屏障

这是写屏障最复杂和关键的应用场景,主要为了解决在垃圾回收器与用户程序并发执行时出现的对象漏标问题。

  1. 并发标记的挑战:漏标

    并发标记算法(如三色标记法)将对象分为白(未访问)、灰(已访问但引用未扫描)、黑(已访问且引用已扫描)三种颜色。漏标问题在并发场景下需要满足两个条件:

    • 赋值器插入了一条或多条从黑色对象指向白色对象的新引用。

    • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

      如果不加干预,这个本应存活的白色对象就会被错误回收,导致程序错误。

  2. 主要的解决方案

    写屏障通过拦截引用写操作来破坏上述条件,主要有两种主流技术:

    • 增量更新:关注插入的新引用。当发生黑色对象 → 白色对象的引用时,写屏障会将这个黑色对象重新标记为灰色。这样,垃圾回收器后续会重新扫描它,从而扫描到新引用的白色对象。采用这种方案的典型回收器是CMS。
    • 原始快照:关注删除的引用。当要删除一个从灰色对象到白色对象的引用时,写屏障会记录下这个被删除的引用。垃圾回收器在后续标记时,会假定这个被删除的引用仍然存在,并以此为基础继续标记那个白色的对象(即基于标记开始时对象图的快照)。采用这种方案的典型回收器是G1。
  3. 具体实现技术

    • 卡表:一种常用的高效实现技术。它将堆内存划分为固定大小的"卡页",并用一个字节数组(卡表)来跟踪这些卡页的状态。当发生可能产生跨代引用的写操作时(如老年代对象引用新生代对象),写屏障会将该对象所在的卡页标记为"脏"。后续进行新生代垃圾回收时,只需扫描被标记为"脏"的卡页,从而避免扫描整个老年代,大大提升了效率。
    • SATB队列:在G1收集器中,写屏障会将被覆盖的旧引用值(即被删除的引用)记录在一个队列中。在标记终止阶段,GC会重新扫描这些记录下的引用,确保不遗漏任何在标记开始后变得不可达的对象。

💾 操作系统与文件系统中的写屏障

在文件系统(如Ext4, XFS)和存储子系统中,写屏障的核心目标是防止在电源故障等异常情况下,文件系统元数据不一致导致的数据损坏

  1. 问题根源:现代存储设备(硬盘、RAID卡)带有易失性写缓存。数据写入缓存后,操作系统就可能被告知写入"完成"。但如果此时断电,缓存中的数据丢失,而文件系统日志的提交块可能已写入磁盘,但实际的事务数据却未写入,这将导致文件系统一致性被破坏。
  2. 写屏障的作用:写屏障通过要求在写入事务数据后、写入提交块之前,强制将缓存数据刷新到持久化存储介质。这确保了数据的写入顺序和持久化符合文件系统的预期,从而保障了元数据的完整性和一致性。

⚙️ 底层内存屏障

在底层硬件和并发编程层面,内存屏障用于控制内存访问的顺序。

  1. 乱序执行:为了提升性能,编译器和处理器会对指令进行重排序。但在多线程环境下,这种重排序可能导致其他线程读到意想不到的数据状态。
  2. 内存屏障类型
    • 写屏障:确保屏障之前的写操作的结果一定在屏障之后的写操作的结果之前,对其他处理器可见。对应的指令如x86架构下的sfence
    • 读屏障:确保屏障之前的读操作一定在屏障之后的读操作之前完成。
    • 全屏障:同时具备读屏障和写屏障的功能。

⚖️ 性能权衡与总结

写屏障是一种典型的以空间换时间以运行时开销换安全性的权衡策略。

  • 开销:每次对引用字段的赋值都会执行额外的屏障代码,带来一定的性能开销。现代系统会采用各种优化(如条件判断、批量处理)来降低开销。
  • 收益:带来的收益是巨大的:垃圾回收器的停顿时间大幅缩短,应用程序响应能力提升;文件系统数据可靠性得到保障;多线程程序行为正确。

总而言之,写屏障作为一种同步屏障指令或代码钩子,是构建现代可靠、高效计算系统的基石技术之一。理解其工作原理,有助于我们更好地进行系统调优和开发高性能应用。

希望以上解释能帮助你全面理解写屏障技术。如果你对某个特定的垃圾回收器(如G1、ZGC)或者特定语言(如Go、Java)中的实现细节有进一步的兴趣,我们可以继续深入探讨。

写屏障实现

CMS(Concurrent Mark-Sweep)垃圾回收器是 HotSpot JVM 中一款以最小化停顿时间为目标的老年低延迟收集器。它主要通过与应用程序线程并发执行大部分垃圾回收工作来实现这一目标。

其核心工作流程包含几个关键阶段,下图清晰地展示了其并发执行和短暂停顿的特点:

flowchart TD
    A[初始标记<br>(Initial Mark)] --> B[并发标记<br>(Concurrent Mark)]
    B --> C[并发预清理<br>(Concurrent Preclean)]
    C --> D[可终止的并发预清理<br>(Abortable Preclean)]
    D --> E[重新标记<br>(Remark)]
    E --> F[并发清理<br>(Concurrent Sweep)]
    F --> G[并发重置<br>(Concurrent Reset)]
    
    subgraph STW [短暂停顿]
        A
        E
    end

    B -- 与用户线程并发执行 --> C
    C -- 与用户线程并发执行 --> D
    F -- 与用户线程并发执行 --> G

以下是每个阶段的详细说明:

🔍 初始标记

这是CMS中的第一次短暂停顿。此阶段仅标记与GC Roots直接关联的对象,以及被新生代中存活对象引用的老年代对象。由于其任务量小,且通常支持多线程并行,因此停顿时间非常短。

🔄 并发标记

在此阶段,GC线程与用户线程并发执行。它从初始标记阶段标记的对象出发,遍历老年代的对象图,标记所有可达的存活对象。由于是并发的,此阶段不会暂停应用线程,但耗时较长。在此期间,对象的引用关系可能发生变化,CMS通过写屏障技术将发生引用变化的内存区域标记为“脏”。

🧹 并发预清理

这个阶段也是并发的,主要目的是处理在并发标记阶段被标记为“脏”的内存区域,重新标记那些引用关系发生变化的对象。这样可以减少后续重新标记阶段的工作量。

⏸️ 可终止的并发预清理

此阶段同样是并发的,它会循环地进行预处理工作,并尝试在新生代使用率达到一定比例(默认50%)前等待一次Minor GC,或者在最长时间限制(默认5秒)内尽可能多地处理脏卡。目的是为了在进入停顿阶段前,让新生代尽可能“干净”,从而显著缩短重新标记阶段扫描新生代的时间。

⚡ 重新标记

这是CMS中的第二次停顿,也是整个过程中通常耗时最长的一次停顿。它需要完成最终的标记,以确保所有存活对象都被正确识别。此阶段会重新扫描GC Roots、新生代中的对象(因为新生代对象可能引用老年代对象)以及之前阶段记录的所有“脏”卡。一个重要的调优参数是 -XX:+CMSScavengeBeforeRemark,它允许在重新标记之前强制进行一次Minor GC,通过清理掉大部分已死亡的新生代对象来减少扫描范围,从而有效缩短此阶段的停顿时间。

🧽 并发清理

在此阶段,GC线程再次与用户线程并发执行,清理那些未被标记的对象(即垃圾对象),并回收它们占用的内存空间。需要注意的是,在此阶段用户线程仍在运行,因此可能会产生新的垃圾,即“浮动垃圾”,这些垃圾将在下一次GC时被清理。

🔄 并发重置

这是CMS周期的最后一个阶段,它是并发的,用于重置CMS收集器内部的数据结构,为下一次垃圾回收周期做好准备。

⚠️ CMS的缺点与调优

尽管CMS在降低停顿时间上表现出色,但它也存在一些固有的缺点和调优考量:

缺点/问题描述与调优建议
内存碎片CMS采用的“标记-清除”算法不会移动存活对象,可能导致内存碎片。可以通过参数-XX:+UseCMSCompactAtFullCollection(默认开启)和-XX:CMSFullGCsBeforeCompaction=n来控制在进行Full GC时进行内存碎片整理的频率。
CPU资源敏感并发阶段会与用户线程竞争CPU资源,可能降低应用总吞吐量。默认启动的回收线程数为(CPU数量 + 3) / 4
并发模式失败当CMS在并发清理过程中,老年代空间被迅速填满,导致没有足够空间容纳从新生代晋升的对象时会发生。可以通过参数-XX:CMSInitiatingOccupancyFraction设置一个合理的阈值(如70%)来提前触发CMS回收,预留足够空间。
浮动垃圾并发清理阶段用户线程产生的垃圾无法在本次收集中被清除,成为“浮动垃圾”。

💎 总结与建议

CMS是一款经典的低延迟垃圾收集器,其核心价值在于通过复杂的并发阶段设计最大限度地减少应用停顿。然而,它也存在内存碎片、对CPU资源敏感等问题。值得注意的是,在较新的JDK版本(如JDK 9及以后)中,CMS已被标记为废弃。对于新项目,建议优先考虑如G1或ZGC等新一代的低延迟垃圾收集器。

希望以上详细的解释能帮助你全面理解CMS垃圾回收器。如果你对特定的调优参数或与其它收集器的对比感兴趣,我们可以继续深入探讨。

CMS

CMS(Concurrent Mark-Sweep)垃圾回收器是 HotSpot JVM 中一款以最小化停顿时间为目标的老年低延迟收集器。它主要通过与应用程序线程并发执行大部分垃圾回收工作来实现这一目标。

其核心工作流程包含几个关键阶段,下图清晰地展示了其并发执行和短暂停顿的特点:

flowchart TD
    A[初始标记<br>(Initial Mark)] --> B[并发标记<br>(Concurrent Mark)]
    B --> C[并发预清理<br>(Concurrent Preclean)]
    C --> D[可终止的并发预清理<br>(Abortable Preclean)]
    D --> E[重新标记<br>(Remark)]
    E --> F[并发清理<br>(Concurrent Sweep)]
    F --> G[并发重置<br>(Concurrent Reset)]
    
    subgraph STW [短暂停顿]
        A
        E
    end

    B -- 与用户线程并发执行 --> C
    C -- 与用户线程并发执行 --> D
    F -- 与用户线程并发执行 --> G

以下是每个阶段的详细说明:

🔍 初始标记

这是CMS中的第一次短暂停顿。此阶段仅标记与GC Roots直接关联的对象,以及被新生代中存活对象引用的老年代对象。由于其任务量小,且通常支持多线程并行,因此停顿时间非常短。

🔄 并发标记

在此阶段,GC线程与用户线程并发执行。它从初始标记阶段标记的对象出发,遍历老年代的对象图,标记所有可达的存活对象。由于是并发的,此阶段不会暂停应用线程,但耗时较长。在此期间,对象的引用关系可能发生变化,CMS通过写屏障技术将发生引用变化的内存区域标记为“脏”。

🧹 并发预清理

这个阶段也是并发的,主要目的是处理在并发标记阶段被标记为“脏”的内存区域,重新标记那些引用关系发生变化的对象。这样可以减少后续重新标记阶段的工作量。

⏸️ 可终止的并发预清理

此阶段同样是并发的,它会循环地进行预处理工作,并尝试在新生代使用率达到一定比例(默认50%)前等待一次Minor GC,或者在最长时间限制(默认5秒)内尽可能多地处理脏卡。目的是为了在进入停顿阶段前,让新生代尽可能“干净”,从而显著缩短重新标记阶段扫描新生代的时间。

⚡ 重新标记

这是CMS中的第二次停顿,也是整个过程中通常耗时最长的一次停顿。它需要完成最终的标记,以确保所有存活对象都被正确识别。此阶段会重新扫描GC Roots、新生代中的对象(因为新生代对象可能引用老年代对象)以及之前阶段记录的所有“脏”卡。一个重要的调优参数是 -XX:+CMSScavengeBeforeRemark,它允许在重新标记之前强制进行一次Minor GC,通过清理掉大部分已死亡的新生代对象来减少扫描范围,从而有效缩短此阶段的停顿时间。

🧽 并发清理

在此阶段,GC线程再次与用户线程并发执行,清理那些未被标记的对象(即垃圾对象),并回收它们占用的内存空间。需要注意的是,在此阶段用户线程仍在运行,因此可能会产生新的垃圾,即“浮动垃圾”,这些垃圾将在下一次GC时被清理。

🔄 并发重置

这是CMS周期的最后一个阶段,它是并发的,用于重置CMS收集器内部的数据结构,为下一次垃圾回收周期做好准备。

⚠️ CMS的缺点与调优

尽管CMS在降低停顿时间上表现出色,但它也存在一些固有的缺点和调优考量:

缺点/问题描述与调优建议
内存碎片CMS采用的“标记-清除”算法不会移动存活对象,可能导致内存碎片。可以通过参数-XX:+UseCMSCompactAtFullCollection(默认开启)和-XX:CMSFullGCsBeforeCompaction=n来控制在进行Full GC时进行内存碎片整理的频率。
CPU资源敏感并发阶段会与用户线程竞争CPU资源,可能降低应用总吞吐量。默认启动的回收线程数为(CPU数量 + 3) / 4
并发模式失败当CMS在并发清理过程中,老年代空间被迅速填满,导致没有足够空间容纳从新生代晋升的对象时会发生。可以通过参数-XX:CMSInitiatingOccupancyFraction设置一个合理的阈值(如70%)来提前触发CMS回收,预留足够空间。
浮动垃圾并发清理阶段用户线程产生的垃圾无法在本次收集中被清除,成为“浮动垃圾”。

💎 总结与建议

CMS是一款经典的低延迟垃圾收集器,其核心价值在于通过复杂的并发阶段设计最大限度地减少应用停顿。然而,它也存在内存碎片、对CPU资源敏感等问题。值得注意的是,在较新的JDK版本(如JDK 9及以后)中,CMS已被标记为废弃。对于新项目,建议优先考虑如G1或ZGC等新一代的低延迟垃圾收集器。

希望以上详细的解释能帮助你全面理解CMS垃圾回收器。如果你对特定的调优参数或与其它收集器的对比感兴趣,我们可以继续深入探讨。

STAB 多标

您提出了一个非常好的问题,这直接触及了并发垃圾回收中正确性和效率的核心权衡。您的直觉是对的:是的,STAB(原始快照)策略确实会导致“多标”,从而产生更多的“浮动垃圾”。但这是一种有意的、可接受的设计取舍。

为了更直观地理解这一过程,下图展示了STAB机制如何通过记录引用删除操作来避免漏标,并同时接受了多标的产生:

flowchart LR
    A[并发标记开始] --> B[生成对象图原始快照]
    B --> C[用户线程断开引用<br>Grey->White]
    C --> D{STAB写屏障介入}
    D -- 是 --> E[记录旧引用<br>(Grey->White)]
    D -- 否 --> F[不干预]
    E --> G[基于原始快照<br>标记White为存活]
    G --> H[结果:避免漏标<br>但产生多标/浮动垃圾]
    F --> I[结果:可能发生漏标]
    H --> J[本轮GC安全]
    I -.-> K[错误:回收存活对象]

下面我们详细解释一下这张图展现的逻辑。

STAB 的核心思路与多标的产生

STAB 的核心目标是:在并发标记阶段,无论用户线程如何改变引用关系,垃圾收集器都坚持按照标记开始时那一刻的对象图快照(Snapshot At The Beginning)来进行追踪

结合上图,这个过程是:

  1. 记录删除:当灰色对象(Grey)指向白色对象(White)的引用被用户线程删除时(例如 a.b.d = null),STAB 的写屏障(写前屏障)会捕获这个即将被删除的引用,并将这个白色对象记录下来。如流程图所示,这相当于冻结了标记开始时的引用关系。
  2. 坚持快照:在后续的重新标记阶段,垃圾收集器不仅扫描GC Roots和灰色对象,还会扫描所有被记录下来的、在快照中存活的白对象。这样,即使这个白色对象在并发标记期间已经没有被任何灰色或黑色对象直接引用了,它依然会被认为是存活的。
  3. 多标的必然结果:这个被强制标记为存活的白色对象,实际上可能已经不再被任何存活对象引用(如上图所示,灰色对象断开引用后,没有其他引用),它已经成了垃圾。但由于STAB机制,它在本轮GC中不会被回收,从而变成了“浮动垃圾”。

为何接受多标?一种主动的权衡

既然STAB会导致多标,为什么G1、Shenandoah等高性能收集器还要采用它呢?这背后是一种典型的工程权衡:

  • 多标(浮动垃圾)的危害是可接受的:多标只是让一些本应本次回收的垃圾对象推迟到下一次GC被回收。这会导致一定的内存空间浪费,但绝不会影响程序的正确性,不会导致程序崩溃。相当于“宁可错放,不可错杀”。
  • 漏标的危害是致命的:反之,如果发生漏标,一个存活对象会被错误地回收,这会导致程序出现难以预测的严重错误,是绝对要避免的。
  • STAB的效率优势:与另一种主流解决方案“增量更新(Incremental Update)”相比,STAB在重新标记阶段的工作量通常更轻。增量更新需要重新深度扫描那些插入新引用的黑色对象,而STAB只需要将记录下来的白色对象直接标记为黑色(或加入待处理集合)即可,处理路径更短,在像G1这样堆内存分布零散(多个Region)的收集器中,效率更高。

与增量更新的对比

为了更清晰地理解这种权衡,可以对比一下CMS收集器使用的增量更新策略:

  • 目标不同:增量更新关注的是新建立的引用。当黑色对象引用了新的白色对象时,它会将黑色对象“降级”为灰色,等待重新扫描。
  • 开销不同:增量更新在重新标记阶段可能需要重新扫描整个黑色对象的新引用,这可能会带来更大的开销,尤其是在对象引用关系复杂的情况下。

总结

所以,您的理解是正确的。STAB通过主动地、保守地保留可能已失效的引用关系(即接受多标/产生浮动垃圾)为代价,来绝对确保并发标记时不会发生致命的漏标问题。 这是一种用微小的空间浪费(浮动垃圾稍后回收)来换取程序绝对正确性和更优暂停时间的巧妙设计。

希望这个解释能帮助您更深入地理解STAB机制的精妙之处!

G1 STAB

G1垃圾收集器中的SATB(Snapshot-At-The-Beginning) 机制是其实现高并发、低停顿垃圾回收的关键。为了更直观地把握其核心实现与优化点,下表总结了SATB机制的关键技术组件及其优化目标。

技术组件/优化点主要职责/优化目标
TAMS指针在并发标记开始时,为每个Region设置NTAMS,用于快速区分哪些对象是标记开始后新分配的,并默认视其为存活。
前一个/下一个位图使用两个位图交替记录标记状态,避免在并发标记过程中直接修改对象标记信息,减少线程冲突。
SATB写屏障在引用被覆写前,捕获旧引用关系,将可能“丢失”的白色对象记录到缓冲区,确保依据标记开始时的快照进行追踪。
SATB队列与缓冲区将写屏障捕获的引用变化先存入线程本地缓冲区,再异步批处理到全局队列,减少STW时间。
并发标记线程处理多个GC线程并行处理全局SATB队列中的记录,重新扫描这些引用,纠正可能发生的漏标。

下面我们具体看看这些技术是如何协同工作的。

🔄 核心实现机制

SATB机制的核心思想是,在并发标记阶段开始时,为堆内存中的存活对象图建立一个逻辑快照。即使之后用户线程修改了引用关系,垃圾收集器在标记时仍会依据这个快照来判断对象的存活。这主要是通过以下几个关键部分实现的。

  1. TAMS指针与位图管理

    G1为每个内存分区(Region)维护了两个特殊的指针:PTAMSNTAMS

    • PTAMS 指向上一次标记周期结束时的位置。

    • 当新一轮并发标记开始时,NTAMS 被设置为当前分区的顶部(Top)。在NTAMS之后分配的对象被认为是本轮标记周期中新分配的。根据SATB的规则,这些新对象会被隐式地视为存活对象,从而简化了对新对象处理的复杂性。

      G1并不将对象的标记状态直接记录在对象头中,而是使用两个位图(Previous Bitmap 和 Next Bitmap)来管理。这种设计避免了在并发标记时直接修改对象元数据带来的冲突,位图操作也更高效。

  2. SATB写屏障

    这是实现SATB的关键。写屏障是在对象引用字段被写入(赋值)前执行的一小段代码钩子。在G1中,它的主要作用是:在一个引用关系被覆写(即失效)之前,将当前引用的对象(通常是白色对象)记录下来。这段逻辑在HotSpot虚拟机源码中通常通过内联方式实现,以最大化减少性能开销。

    例如,当执行 a.d = b(原本是 a.d = c)时,在覆盖引用 a.d之前,写屏障会捕获旧的引用值 c(如果 c是白色对象),并将其记录到待处理队列中。

⚙️ 关键性能优化

为了降低SATB机制带来的开销,G1进行了一系列精妙的优化。

  1. 异步处理的缓冲区机制

    写屏障并不会在每次触发时都进行复杂的全局操作。它会先将需要记录的信息(如旧的引用值或对应的卡表索引)放入当前线程本地的SATB缓冲区。当该缓冲区被填满时,才会将该缓冲区加入一个全局列表,并分配一个新的本地缓冲区继续工作。这个将本地缓冲区发布到全局列表的过程通常需要短暂的停顿,但将频繁的写操作转换为了批量的异步处理,极大地减少了性能损耗。

  2. 并发标记线程的处理

    在并发标记阶段,有专门的GC线程会去处理全局的SATB队列。它们会逐个取出缓冲区,并扫描其中记录的旧引用。通过这种方式,确保那些在标记开始后可能因引用关系改变而“丢失”的白色对象能够被重新标记,从而在最终的“重新标记阶段”只需要处理极少数量的剩余对象,大大缩短了STW停顿的时间。

  3. 与卡表和RSet的协同

    SATB机制与G1的另外两个核心数据结构紧密协作:

    • 卡表:SATB写屏障有时会通过标记卡表(Dirty Card) 的方式来实现。当发生可能重要的引用更新时,写屏障会将对应的内存区域标记为“脏”。后续的并发标记线程会扫描这些脏卡区域,从而处理其中的引用变化。
    • 记忆集:每个Region都有一个RSet,用于记录外部Region对本Region的引用。在垃圾回收时,G1可以借助RSet高效地找出哪些Region包含了指向待回收Region的引用,避免扫描整个堆。SATB机制与RSet协同,确保了在并发标记期间,即使有跨Region的引用被修改,相关的可达性信息也能被正确维护,提高了回收的精确度。

💎 总结与价值

总而言之,G1收集器的SATB机制通过TAMS指针/位图、写屏障、异步缓冲区以及与卡表/RSet的协同等一系列精妙的设计与优化,在保证并发标记正确性(避免漏标)的前提下,最大限度地减少了垃圾收集对应用线程的停顿影响。这种“在开始时保留快照”的保守策略,虽然可能会产生一些额外的浮动垃圾,但它换来了停顿时间的可控性,这正是G1成为一款面向低延迟场景的垃圾收集器的基石。

希望这些详细的解释能帮助你更深入地理解G1收集器的工作原理。如果你对G1的其它方面,比如Mixed GC的回收策略或者调优参数还有兴趣,我们可以继续探讨。

G1

G1(Garbage-First)是一款旨在平衡高吞吐量与低停顿时间的垃圾收集器,尤其适合大内存堆。其核心在于将堆划分为多个固定大小的 Region,并基于可预测的停顿时间模型,优先回收价值最高(即垃圾最多)的 Region 。

下图直观展示了 G1 收集器的一次完整并发标记周期及其后续的混合收集流程,其中包含了 STW 暂停和并发阶段的关键步骤与核心数据结构,你可以结合此图来理解下文将详细介绍的各个阶段。

flowchart TD
    A[堆占用达阈值<br>IHOP] --> B

    subgraph B [初始标记 STW]
        B1[标记GC Roots<br>直接可达对象]
    end

    B --> C[根区域扫描<br>(并发)]
    C --> D[并发标记]
    D --> E[最终标记 STW]
    E --> F[清理 STW]
    F --> G{Mixed GC<br>混合回收 STW}
    
    B --> H[年轻代GC<br>搭载初始标记]
    H --> I[年轻代GC<br>常规发生]
    I --> H

    subgraph GcLoop [混合回收循环]
        direction LR
        G1[选择回收价值高的Region<br>构成CSet] --> G2[复制存活对象<br>到空闲Region]
    end

    G --> GcLoop
    GcLoop --> GcLoop

下面我们详细看看 G1 的各个阶段。

🔄 G1 的核心工作阶段

G1 的收集活动主要包括年轻代收集(Young GC)和混合收集(Mixed GC),而混合收集的准备阶段是一个关键的并发标记周期

年轻代收集

年轻代收集主要负责回收 Eden 区和 Survivor 区 。

  • 触发条件:当 Eden 区的 Region 被耗尽时便会触发 。这是一个 Stop-The-World (STW) 事件,但采用多线程并行方式加速完成 。
  • 主要工作:这个过程会将 Eden 区和 Survivor 区中存活的对象复制到新的 Survivor Region,并提升年龄;当对象年龄超过阈值(默认15)或 Survivor 空间不足时,存活对象会晋升到老年代 Region 。同时,它会更新记忆集,记录下老年代到新生代的引用 。

并发标记周期

这是 G1 回收老年代的基础,目的是找出所有存活对象,识别出可回收的 Region 。它并非立即执行回收,而是为后续的混合收集做准备 。

  1. 初始标记:此阶段标记所有从 GC Roots 直接可达的对象 。这是一个短暂的 STW 停顿,且通常搭载在一次普通的年轻代收集过程中同步完成,因此几乎没有额外停顿开销 。
  2. 根区域扫描:在初始标记后,G1 开始扫描 Survivor Region 中引用的老年代对象 。此阶段必须在下一次年轻代收集发生前完成
  3. 并发标记:从 GC Roots 开始,并发地(与用户线程一起)遍历整个堆,标记所有存活的对象 。此阶段若发现某个 Region 中全是垃圾,则会立即回收 。
  4. 最终标记:处理并发标记阶段用户线程变更引用关系后遗留下来的少量 SATB 记录,确保标记结果的准确性 。这是一个 STW 阶段,但通常很快 。
  5. 清理阶段:此阶段会统计各 Region 中存活对象的比例,并排序回收价值,识别出完全空闲的 Region 进行回收 。这部分工作部分 STW,部分可并发执行 。

混合收集

在并发标记周期结束后,G1 并不会一次性回收所有标记出来的老年代 Region。它会启动一系列的 混合收集

  • 触发条件:当老年代 Region 在堆中的占用比例达到阈值(默认 45%,由 -XX:InitiatingHeapOccupancyPercent设定)时触发 。
  • 工作方式:每次混合收集,G1 会选择一部分回收价值最高的老年代 Region,与年轻代 Region 一起,构成本次的收集集(CSet) 。然后采用 复制算法,将 CSet 中所有存活对象复制到新的空闲 Region,同时清空原 Region 。这个过程是 STW 的 。混合收集会多次进行,直到回收了足够多的老年代 Region,使堆占用率下降到预期目标 。

⚙️ G1 的关键优化技术

G1 实现高效回收,离不开其核心组件的协同工作 。

  • 记忆集:每个 Region 都有一个记忆集(RSet),用于记录其他 Region 中的对象指向本 Region 对象的引用 。这样在进行垃圾收集时,无需扫描整个堆,只需扫描 RSet 即可确定哪些对象是存活的,大大提高了效率 。
  • SATB 算法:G1 使用 Snapshot-At-The-Beginning 算法来解决并发标记期间对象引用变化可能导致的“漏标”问题 。其核心思想是在标记开始时为存活对象图建立一个逻辑快照,即使之后引用关系变化,垃圾收集器在本次标记时仍会依据快照来判断 。这通过写屏障实现 。

🎯 调优参数与适用场景

合理配置 G1 参数有助于提升性能 。

参数作用建议
-XX:+UseG1GC启用 G1 垃圾收集器如需使用 G1,则必须开启
-XX:MaxGCPauseMillis设置期望的最大 GC 停顿时间目标(毫秒)默认 200ms,可根据应用响应要求调整
-XX:InitiatingHeapOccupancyPercent设置触发并发标记周期的堆占用阈值(百分比)默认 45%,若老年代增长快可适当调低
-XX:G1HeapRegionSize设置每个 Region 的大小通常默认即可,堆非常大时可考虑增大
-XX:ConcGCThreads设置并发阶段(如并发标记)使用的 GC 线程数根据 CPU 资源情况调整

G1 尤其适用于以下场景 :

  • 堆内存较大(如超过 6GB)。
  • 应用对停顿时间敏感,希望避免长时间 GC 停顿。
  • 需要兼顾吞吐量和延迟

💎 总结与提醒

G1 通过 Region 分区并发标记混合收集机制,实现了可预测的停顿时间和高效的内存回收。理解其工作流程和核心机制(如 RSet 和 SATB)是进行有效监控和调优的基础。需要警惕的是,在极端情况下(如并发模式失败或晋升失败),G1 可能触发 Serial Old GC 进行 Full GC,这将导致长时间的 STW 。因此,合理设置 JVM 参数、监控 GC 日志至关重要。

希望这些详细的解释能帮助你更深入地理解 G1 垃圾收集器。如果你对特定的调优场景或有其他相关问题,我们可以继续探讨。

STAB 卡表

您这个问题触及了现代垃圾回收器并发标记实现的核心细节。简单来说,这个区别的关键在于:虽然两者都可能使用卡表作为底层工具,但它们利用卡表所要解决的问题、触发的逻辑以及最终的标记范围有着本质的不同

下面这个表格清晰地对比了这两种机制的核心差异。

对比维度SATB(用于G1等)的卡表标记增量更新(用于CMS)的卡表标记
核心目标记录可能失效的旧引用,防止因引用删除而漏标。记录新增的新引用,防止因引用插入而漏标。
触发条件写屏障检测到引用被赋值前的值(即旧引用)是否为白色对象。写屏障检测到黑色对象是否插入了指向白色对象的新引用
卡表记录内容存放旧引用所在的内存卡页,意味着“这个卡页里可能有引用关系变了,需要检查快照”。存放插入新引用的黑色对象所在的内存卡页,意味着“这个卡页里的黑色对象需要重新扫描”。
后续处理重新标记阶段,将记录下的旧引用作为根,仅扫描这些引用,不重新扫描整个堆重新标记阶段,需要重新扫描所有脏卡中的黑色对象(即深度扫描),工作量大。
设计哲学效率优先:接受更多浮动垃圾,换取更短、更稳定的重新标记停顿。精度优先:试图更精确地追踪变化,但可能导致重新标记阶段扫描范围大、耗时长。

🔍 深入解析差异根源

为了更深入地理解上述差异,我们需要探究其背后的根源。

  1. 解决不同的问题

    • SATB 的核心思想是破坏导致漏标的第二个条件(删除灰色对象到白色对象的引用)。它关注的是“消失的引用”。因此,它的写屏障(写前屏障)会在一个引用被覆盖(即一个旧引用关系即将失效)之前,捕获这个旧引用值。如果这个旧引用指向的对象是白色的,就会被记录起来。这样,在后续标记时,GC会认为这个旧引用仍然存在,从而避免白色对象被漏标。
    • 增量更新 的核心思想是破坏导致漏标的第一个条件(黑色对象引用了白色对象)。它关注的是“新建立的引用”。因此,它的写屏障会在黑色对象插入指向白色对象的新引用时,将这个黑色对象“降级”为灰色。这样,GC后续会重新扫描这个灰色对象,从而扫描到新引用的白色对象。
  2. 性能与影响的权衡

    这个差异直接导致了它们性能特征的不同:

    • SATB的轻量性:SATB记录的是单个的旧引用。在最终的重新标记阶段,GC线程只需要以这些记录下来的引用为根,进行一轮相对简单的扫描即可。这使得其重新标记阶段的停顿时间更短、更可控。代价是会产生更多的“浮动垃圾”(即那些本来已经失效,但被SATB快照保留下来的对象),但这通常是可以接受的。
    • 增量更新的开销:增量更新将整个黑色对象标记为待重新扫描。在重新标记阶段,CMS需要从这些“脏卡”出发,重新深度扫描这些黑色对象的所有新引用。如果这个黑色对象是一个位于复杂引用链顶部的根对象,那么重新扫描它的开销可能会很大,导致CMS的“重新标记”阶段停顿时间较长且不易预测。

💎 总结

总而言之,“SATB写屏障通过标记卡表实现”与“CMS增量更新标记卡表”的根本区别,源于它们为解决并发标记漏标问题所采取的不同策略

  • SATB 像一个档案管理员,它在文件被修改前拍下快照存档(记录旧引用)。需要时只查阅存档即可,速度快,但存档里可能会保留一些已作废的文件(浮动垃圾)。
  • CMS增量更新 像一个实时监控员,它发现新连接建立时(黑色对象引用白色对象),就标记整个源头区域(将黑色对象降级)。检查时需要把这个区域重新排查一遍(重新扫描黑色对象),工作量大,但理论上更实时。

这也是为什么G1等现代收集器倾向于采用SATB的原因之一,因为它更适合大内存和需要低延迟停顿的场景。

希望这个解释能帮助您彻底理解这两者的微妙差别

ZGC 着色指针

三色标记法是垃圾回收器进行并发标记的核心算法,而ZGC通过创新的着色指针读屏障技术,极大地优化了这一过程的效率和并发能力,从而实现了其标志性的亚毫秒级低停顿目标。

🎨 理解核心优化技术

ZGC对三色标记法的优化主要基于两项核心技术:

  1. 着色指针

    传统GC将对象的状态(如三色标记中的黑、灰、白)记录在对象的标记位存储在对象头中,访问时需要先读内存,容易成为瓶颈。ZGC则另辟蹊径,它将标记信息直接编码到对象的64位内存地址指针的高4位中。这4个特殊位(称为元数据位)用来表示对象的标记状态(如 Marked0Marked1)和重定位状态(如 Remapped)。这意味着,通过查看指针本身的值,ZGC就能立刻知道对象当前的状态,无需访问可能已被CPU缓存淘汰的对象内存,速度极快。

  2. 读屏障

    读屏障是JVM在应用线程从堆中读取对象引用时自动插入的一小段代码钩子。在ZGC中,它的核心职责是检查引用指针上的元数据位。根据GC所处的不同阶段(并发标记或并发转移),读屏障会判断指针的颜色状态,并执行相应的操作,比如将未标记的对象放入待处理队列,或者修正指向已移动对象的旧引用。

⚙️ 优化如何在实际回收阶段发挥作用

下面我们看看这两项技术如何在ZGC的垃圾回收周期中协同工作。

阶段角色与协作方式
并发标记阶段着色指针:充当状态标签,GC线程通过原子操作更改指针颜色(如从Remapped设为Marked0)来标记对象。 读屏障:充当监控器与校正器,应用线程访问引用时,读屏障检查指针颜色。若发现未被标记的(白色)对象,则将其标记为灰色(加入标记栈)。
并发转移阶段着色指针:标识对象是否已被转移(Remapped状态)。 读屏障:应用线程访问引用时,若读屏障通过指针颜色发现该对象已被转移,它会触发“自愈”逻辑,自动将引用更新到新地址,并修正指针颜色。

💎 优化带来的关键优势

ZGC通过着色指针和读屏障优化三色标记法,带来了显著优势:

  • 极高的标记效率:直接操作指针位比访问对象头更快,降低了并发标记的开销。
  • 出色的并发性能:读屏障的“自愈”特性大幅减少了停顿时间,使得ZGC的停顿几乎独立于堆大小。
  • 精准的状态判断:从指针层面获取状态,判断更加直接可靠。

希望以上解释能帮助你理解ZGC中的这项精妙设计。如果你想更深入地了解ZGC的其他阶段或者与其他垃圾回收器的细节对比,我们可以继续探讨。

Licensed under CC BY-NC-SA 4.0
Last updated on Oct 22, 2025 16:26 CST
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy