协助标记 (Mark Assist) 是 Go GC 的一种背压 (Backpressure) 机制。当业务协程分配内存太快,导致 GC 扫描跟不上时,运行时会强制该协程“停工”,转去帮 GC 扫垃圾。
为什么需要协助标记?
在并发 GC 期间,系统面临一个经典的生产与消费平衡问题:
| 角色 | 行为 | 潜在风险 |
|---|---|---|
| 业务协程 (User) | 持续生产(分配内存) | 如果分配太快,堆内存会瞬间撑爆。 |
| GC 后台协程 (Worker) | 持续消费(扫描标记) | 如果扫描太慢,回收速度赶不上增长速度。 |
如果任由业务协程“狂奔”,GC 还没标记完,程序就可能因为 OOM (Out of Memory) 崩溃。
核心逻辑:谁污染,谁治理
为了解决上述问题,Go 引入了 Mark Assist。它通过一套“信用分”系统来强制平衡速度:
| 行为 | 对信用分 (Credit) 的影响 | 结果 |
|---|---|---|
| 申请内存 (Malloc) | 扣分 (消耗信用) | 信用分为负时,协程被扣留。 |
| 协助标记 (Scan) | 加分 (赚取信用) | 必须通过“扫大街”赚分才能继续分配。 |
协助标记的作用
- 提供背压 (Backpressure):通过强制暂停分配过快的协程,从源头上限制内存增长速率。
- 加速标记:将原本只有几个后台协程在干的活,分摊给所有“高产”的业务协程,确保标记阶段按时完成。
- 防止 OOM:确保在堆内存触及目标阈值前,所有的存活对象都已被标记完成。
对应用的影响
- 局部性影响:它不是 STW,只会影响那个“手速太快”的 Goroutine,其他协程正常运行。
- 性能特征:是导致 P99 延迟 升高的主要原因。如果你发现某个接口偶尔响应极慢,可能就是它在帮 GC “打工”。
运行流程
graph TD A[Goroutine 申请分配内存] --> B{信用分是否足够?} B -- 足够 --> C[直接分配并扣分] B -- 不足 --> D[进入 Mark Assist 模式] D --> E[帮 GC 扫描一部分内存] E --> F[赚取信用分] F --> B
一句话总结:Mark Assist 就像是交警拦下了超速车辆,并要求司机下车帮扫大街,直到补齐“环保贡献”才能继续开车。