JVM垃圾回收机制深度解析(G1篇)(垃圾回收过程及专业名词详解)(补充)

本文将补充讲解JVM垃圾回收机制深度解析(G1篇)(垃圾回收过程及专业名词详解)-CSDN博客这篇文章中没讲到的一些细节:

首先:

那么问题时:图中的并发标记阶段,存活的对象在TAMS指针前还是后?

在G1垃圾回收器中,并发标记阶段 存活的对象位于 TAMS指针之后

关键机制解析

  1. TAMS的定义

    TAMS(Top at Mark Start)是G1回收器中的一个指针,它标记了并发标记开始时堆内存的"顶部"。

    • TAMS之前(下方): 存放的是并发标记开始时就已经存在的对象。G1会扫描这些区域来标记存活对象。
    • TAMS之后(上方): 存放的是在并发标记过程中,应用程序线程新分配的对象。
  2. 隐式标记

    在并发标记阶段,应用程序线程依然在运行并分配新对象。G1不需要专门去"标记"这些新对象为存活,而是采用了一种更高效的方式:

    • 新分配的对象直接被分配在TAMS指针之上的区域。
    • G1回收器默认认为 TAMS之上的所有对象都是存活的(即"隐式存活")。

总结

  • TAMS之前: 需要通过遍历对象图来显式标记哪些是存活的,哪些是垃圾。
  • TAMS之后: 所有对象默认视为存活(因为它们是在标记开始后新创建的)。

问题又来了:最终标记阶段什么作用?

G1垃圾回收器中的 最终标记阶段(Final Marking) 是整个并发标记周期中最关键的 Stop-The-World(STW) 阶段之一,其核心作用是:

修正并发标记期间因用户线程并发修改对象引用而导致的"漏标"问题,确保所有真正存活的对象都被准确标记。


一、为什么需要最终标记?

并发标记阶段(Concurrent Marking) ,GC线程与应用线程并行运行。这虽然减少了停顿时间,但也带来了风险:
应用线程可能在标记过程中修改对象引用关系,例如:

  • 删除一个老对象对某对象的引用(使其本应变为垃圾);
  • 新增一个老对象对某白色对象的引用(使其本应存活却被漏标)。

如果不处理这些变化,就可能导致:

  • 错误回收存活对象(严重!会导致程序崩溃);
  • 或者产生大量"浮动垃圾"(影响内存效率)。

因此,必须在一个 一致的内存快照下 进行一次全局修正 ------ 这就是 最终标记阶段 的使命。


二、最终标记阶段的核心工作

1. 处理 SATB(Snapshot-At-The-Beginning)日志
  • 在并发标记开始时,G1通过写屏障记录了"初始快照"。
  • 并发期间,任何被 断开引用 的对象(即原引用的目标)会被写屏障捕获,并加入 SATB 缓冲队列
  • 在最终标记阶段,GC会 遍历所有 SATB 队列,将这些"可能被漏标的对象"重新标记为存活(通常标记为灰色,加入标记栈继续传播)。

✅ 目的:防止因引用删除导致的"误回收"。

2. 扫描脏卡(Dirty Card Scanning)并更新记忆集(RSet)
  • 并发期间,跨 Region 引用的变更会通过写屏障标记对应的 卡表(Card Table)为"脏"
  • 最终标记阶段会 扫描所有脏卡 ,从中提取出 新增的跨 Region 引用
  • 利用这些引用,更新目标 Region 的 Remembered Set(RSet) ,并 重新扫描这些引用指向的对象,确保它们被正确标记。

✅ 目的:防止因新增引用导致的"漏标"。

3. 完成三色标记的收尾
  • 将所有待处理的灰色对象继续标记,直到标记栈为空。
  • 确保整个堆的标记状态达到 一致且完整
4. 清理与元数据更新
  • 清理无效的 RSet 条目;
  • 更新每个 Region 的 存活对象统计信息(用于后续 Mixed GC 的 Region 选择);
  • 为接下来的 筛选回收(Evacuation)阶段 准备数据。

三、总结:最终标记阶段的作用

表格

作用 说明
保证标记准确性 修正并发期间引用变更造成的漏标或误标
处理 SATB 日志 回收"快照中存在但后来被断开引用"的对象,避免误删
同步 RSet 状态 确保跨 Region 引用被完整记录,支持精确回收
生成回收决策依据 统计各 Region 存活率,为 Mixed GC 选择高收益 Region

💡 关键点 :最终标记是 短暂停顿但高价值 的 STW 阶段,它牺牲少量暂停时间,换取了标记结果的 强一致性安全性,是 G1 实现低延迟、高可靠 GC 的基石。

简言之:最终标记 = 并发标记的"纠错+收尾"阶段,确保"该活的都活,该死的才死"。

最后一个问题:

如果在并发标记阶段,删除了一个老对象对某对象的引用,最终标记阶段会怎么处理?如果在并发标记阶段,是新增一个老对象对某白色对象的引用,最终标记阶段会怎么处理?

回答如下:

背景知识:G1 使用 SATB(Snapshot-At-The-Beginning)机制

G1 的并发标记基于 SATB(快照在开始时) 算法。其核心思想是:

在并发标记开始时拍一个"逻辑快照",所有在这个快照中存活的对象,即使后来变成不可达,也不能被回收。

为了实现这一点,G1 通过 写屏障(Write Barrier) 在对象引用发生变更时记录相关信息。


场景一:删除一个老对象对某对象的引用

示例:

java 复制代码
// 并发标记开始前:A(黑色/已标记) → X(白色)
// 并发标记期间,应用线程执行:
A.ref = null;  // 删除 A 对 X 的引用

问题:

X 是否会被错误回收?

→ 如果 X 没有其他引用,它在快照中是可达的(因为 A 引用了它),但后来被断开。

→ 根据 SATB 原则 ,X 仍然必须被视为存活,因为它在快照中是可达的!

最终标记阶段如何处理?

  • A.ref = null 执行时,写屏障会捕获"被覆盖的旧值" ,即 X
  • 这个 X 会被加入 SATB 缓冲队列(satb_mark_queue)
  • 最终标记阶段(STW) ,GC 线程会:
    1. 遍历所有线程的 SATB 队列;
    2. X 重新标记为灰色(如果尚未标记);
    3. 继续从 X 出发遍历其子对象,确保整个子图都被标记。

结果 :X 不会被回收,符合"快照语义"------保证了 不误删(no false negatives)。
注意:即使 X 后来真的变成垃圾,也要等到 下一轮 GC 周期 才能回收。这是 SATB 的代价,但换来的是安全性。


场景二:新增一个老对象对某白色对象的引用

示例:

java 复制代码
// 并发标记期间,应用线程执行:
B.ref = Y;  // B 是已标记的黑色老对象,Y 是白色对象(尚未被标记)

问题:

Y 是否会被漏标?

→ B 已经是黑色(标记完成),不会再被扫描;

→ Y 是白色,且没有其他灰色对象引用它;

→ 如果不处理,Y 会被当作垃圾回收 ------ 严重错误!

最终标记阶段如何处理?

这里的关键在于:G1 的写屏障不仅记录"被删除的引用",也记录"新增的跨代/跨 Region 引用"

具体流程:

  1. B.ref = Y 执行时:
    • 如果 BY 属于 不同 Region (尤其是老年代 → 老年代 或 老年代 → 新生代),写屏障会将 B 所在的 Card 标记为"脏"
  2. 最终标记阶段
    • GC 会扫描所有 脏卡(Dirty Cards)
    • 通过 Remembered Set(RSet) 重建 B 所在 Region 的外部引用;
    • 发现 B → Y 这条新引用;
    • Y 标记为灰色(如果仍是白色),并加入标记栈继续传播。

结果 :Y 被正确标记为存活,避免了 漏标(missed live object)。
补充:如果 BY同一个 Region,且该 Region 正在被完整扫描,则可能不需要额外处理。但 G1 通常仍会通过卡表机制保守处理。


总结对比

场景 操作 风险 G1 处理机制 最终标记阶段动作
删除引用 A.ref = null 快照中存活的对象被提前回收 SATB 写屏障 捕获旧值 X 从 SATB 队列取出 X,重新标记
新增引用 B.ref = Y(B 黑,Y 白) Y 被漏标回收 卡表 + RSet 记录新跨 Region 引用 扫描脏卡,发现 B→Y,标记 Y

核心结论

G1 通过 双重保障机制 解决并发标记中的引用变更问题:

  1. SATB(快照机制) → 解决 "删除引用" 导致的 误回收
  2. 卡表 + Remembered Set → 解决 "新增引用" 导致的 漏标

最终标记阶段 正是将这两种机制收集到的信息 统一处理、完成标记闭环 的关键 STW 阶段。

因此,无论是引用删除还是新增,G1 都能在最终标记阶段确保:所有在逻辑快照中或之后应存活的对象,都不会被错误回收。

相关推荐
程序员Terry1 小时前
Java 代理模式:从生活中的"中介"到代码中的"代理人"
后端·设计模式
白宇横流学长1 小时前
基于SpringBoot实现的信息技术知识赛系统设计与实现【源码+文档】
java·spring boot·后端
ZHOUPUYU1 小时前
PHP异步编程实战ReactPHP到Swoole的现代方案
开发语言·php
yhyyht1 小时前
Maven命令学习记录(一)
后端
2501_945423541 小时前
如何为开源Python项目做贡献?
jvm·数据库·python
Soofjan2 小时前
Go channel源码
后端
2401_884563242 小时前
使用Flask快速搭建轻量级Web应用
jvm·数据库·python
2301_776508722 小时前
C++中的组合模式变体
开发语言·c++·算法
Soofjan2 小时前
channel
后端