GC全场景分析
文章目录
首先,GC 是一种垃圾处理机制。实现的方式由很多种算法:标记-清除法、三色标记法、屏障机制、混合写屏障机制等。下面,我将从go的角度,go实现过的gc算法进行学习记录和分析。
标记-清除法
标记 - 清除法核心流程与 STW 机制
核心动作:STW(Stop The World)
- 定义:暂停所有应用程序线程,仅允许 GC 线程执行,确保垃圾回收过程中对象引用关系不变。
- 目的:
✅ 避免并发修改导致 浮动垃圾(应回收的对象未被标记)或 误标记(不可达对象被错误标记为存活)。
✅ 简化 GC 算法逻辑,无需处理多线程竞争问题。
标记 - 清除法四步流程
1. STW 启动(暂停用户线程)
- 操作
- GC 线程向所有应用线程发送暂停请求。
- 线程执行到 安全点(Safepoint,如方法调用、循环结束处) 时主动暂停,确保此时对象引用稳定。
- 关键点
- 安全点机制避免线程在非稳定状态被暂停(如正在修改对象引用时)。
2. 标记可达对象(从根集合出发)
- 标记逻辑
- 从 根集合(Root Set) 开始,递归遍历所有 可达对象,标记为 存活(而非 “空闲”)。
- 根集合组成:
▶ 栈内存:线程栈中的局部变量、方法参数(对象引用)。
▶ 方法区:静态变量、类的 Class 对象。
▶ 寄存器:CPU 寄存器中存储的引用。
▶ JNI 引用:本地代码持有的全局对象引用。
- 核心作用:
通过根集合确定所有存活对象,未被标记的对象判定为可回收垃圾。
3. 清除未标记对象(回收堆内存)
- 操作:
扫描整个堆内存,释放所有未被标记的对象占用的空间。 - 副作用:
❗ 产生 内存碎片(不连续的空闲块),可能导致后续大对象分配失败,触发更多 GC。
4. 恢复用户线程(结束 STW)
- 操作:
GC 完成后,通知所有应用线程从安全点继续执行。 - 影响:
STW 期间应用无响应,过长的 STW 会导致系统延迟升高(如实时系统、高并发服务)。
根集合(Root Set)深度解析
1. 本质与作用
- 定义:GC 标记的 初始起点集合,包含所有 直接可达的对象引用。
- 核心作用:
✅ 作为递归遍历的起点,确保所有存活对象被标记。
✅ 隔离堆内存:GC 仅需关注根集合可达的对象,无需扫描整个堆。
2. 分布与组成
内存区域 | 根集合成员示例 |
---|---|
栈内存 | 方法内的局部变量(如Object obj = new Object(); ) |
方法区 | 静态变量(如static Object staticObj; )、类的 Class 对象 |
寄存器 | CPU 寄存器中存储的对象引用(由 JVM 底层管理) |
JNI 本地堆 | 本地代码通过NewGlobalRef 创建的全局引用 |
3. 与 GC 的关键关系
标记阶段的起点
根集合 → 对象A → 对象B → 对象C(标记为存活) ↘ 对象D(标记为存活)
未被根集合直接或间接引用的对象(如孤立对象)将被清除。
STW 的必要性:
若不暂停线程,根集合中的引用可能在标记过程中被修改(如赋值为null
),导致标记结果错误。
对比与总结
维度 | 标记 - 清除法 | 根集合特点 |
---|---|---|
STW 作用 | 确保标记 / 清除阶段对象引用不变 | 分布在栈、方法区、寄存器等多区域 |
标记对象 | 存活对象(可达对象) | 包含静态变量、局部变量等直接引用 |
内存碎片 | 会产生 | 无需移动对象,仅标记 - 清除 |
优化方向 | 结合分代 GC、增量标记减少 STW 时间 | 减少静态变量引用,避免长生命周期对象 |
核心结论:
- 标记 - 清除法通过 STW 和根集合实现垃圾回收,适用于简单场景,但需注意内存碎片问题。
- 根集合是 GC 的 “起点”,其组成与内存分布直接影响 GC 效率,合理管理根引用(如避免冗余静态变量)可优化 GC 性能。
三色标记法与屏障机制
基本概念
- 三色定义
- 白色:初始状态,未被垃圾回收器(GC)访问的对象
- 灰色:已被 GC 访问,但内部引用的对象还未全部处理完
- 黑色:已被 GC 完全处理,其引用的对象均已扫描完成
标记流程
- 初始标记(STW 阶段)
- 暂停所有用户线程
- 从根集合(全局变量、栈上变量等)出发,标记所有直接可达对象为灰色,并放入灰色队列
- 并发标记
- 用户线程与 GC 线程同时运行
- 处理灰色队列
- 取出灰色对象,将其引用的白色对象标记为灰色并加入队列
- 该灰色对象标记为黑色
- 写屏障
- 删除屏障:当灰色对象删除对白色对象的引用时,强制将白色对象标记为灰色
- 插入屏障:黑色对象新增白色对象引用时,将白色对象标记为灰色
- 最终标记(STW 阶段)
- 短暂暂停用户线程
- 处理并发阶段遗留的少量标记任务,确保标记完整性
- 清除阶段
- 回收所有白色对象(即不可达对象)
- 重置黑色和灰色对象为白色,为下次 GC 做准备
混合写屏障机制
Go 语言从 1.8 版本开始引入混合写屏障,主要解决传统屏障的局限性:
- 插入屏障(Insert Barrier):黑色对象插入白色对象引用时需标灰,但栈上对象扫描需 STW(因栈不适用写屏障)。
- 删除屏障(Delete Barrier):灰色对象删除白色对象引用时需标灰,但可能导致浮动垃圾(本轮 GC 未回收)。
混合写屏障的目标:
- 减少 STW 时间:避免扫描栈时的 STW。
- 简化 GC 流程:合并插入与删除屏障的优势。
混合写屏障的核心规则
混合写屏障同时应用以下两条规则:
插入屏障:
当黑色对象引用白色对象时,强制将白色对象标记为灰色。A(黑).field = B(白) → B被标记为灰色
删除屏障(弱化版):
当对象(无论黑白)**删除对**白色对象的引用时,将该白色对象标记为灰色。A(黑/灰).field = nil(原指向B(白)) → B被标记为灰色
栈保护:
栈上对象的引用变更不触发写屏障,但 GC 开始时会对栈进行并发扫描,扫描完成后栈上对象视为黑色(不再二次扫描)。
混合写屏障的 GC 流程改进
- 初始标记(STW)
- 扫描根对象(全局变量、寄存器),标记直接可达对象为灰色。
- 栈扫描:并发扫描所有 goroutine 的栈,标记栈上对象(但不扫描其引用的对象)。
- 并发标记
- 处理灰色队列,标记对象为黑色。
- 写屏障确保:
▶ 黑色对象新增白色引用时,白色对象被标灰。
▶ 对象删除白色引用时,白色对象被标灰。 - 栈处理:
栈上对象在初始扫描后视为黑色,后续变更不触发屏障(依赖初始扫描的正确性)。
- 最终标记(STW)
- 仅需短暂 STW,处理少量未完成的标记任务(如栈上残留的白色对象),无需重新扫描栈。
- 清除阶段
- 并发清除所有白色对象。
关键优势
- 减少 STW 时间
- 栈扫描与用户线程并发进行,仅需初始 STW 扫描一次,无需像插入屏障那样在标记结束后重新 STW 扫描栈。
- 简化 GC 流程
- 统一处理堆上的插入和删除操作,逻辑更简洁。
- 降低内存压力
- 相比删除屏障,混合写屏障减少了浮动垃圾的产生(因插入屏障的存在)。
与传统屏障的对比
特性 | 插入屏障 | 删除屏障 | 混合写屏障 |
---|---|---|---|
栈扫描 STW | 需要两次(初始 + 最终) | 仅初始一次 | 仅初始一次(扫描一次栈全黑,使得无需二次扫描) |
浮动垃圾 | 无 | 有(下轮 GC 回收) | 极少(插入屏障抑制) |
写屏障复杂度 | 仅处理插入操作 | 仅处理删除操作 | 同时处理插入和删除 |
Go 版本 | 1.5-1.7 | 历史方案(未实际使用) | 1.8+ |
示例说明
// 场景:黑色对象A引用白色对象B,同时灰色对象C删除对B的引用
A.field = B // 混合写屏障:B被标记为灰色(插入规则)
C.field = nil // 混合写屏障:B已被标灰(插入规则已处理),无需重复操作
通过混合写屏障,B 在被 A 引用时已被标灰,即使 C 删除引用,B 仍会被正确标记为存活对象。
总结
混合写屏障是 Go 语言 GC 的核心优化,通过合并插入与删除屏障的优势,大幅减少 STW 时间,同时降低内存压力。其核心设计思想是:
- 并发栈扫描:避免栈操作的写屏障,通过初始 STW 扫描一次栈。
- 堆上双重保护:通过插入和删除规则,确保引用变更时对象被正确标记。
- 最终 STW 优化:仅需短暂暂停处理残留任务,无需重新扫描栈。
这使得 Go 语言的 GC 在保证正确性的同时,实现了极致的低延迟,特别适合高并发场景。