--- tags: [test/review, go, gc, tri-color-marking, write-barrier] create time: 2026-08-09 12:00 --- # 三色标记 GC 原理_测试题 ## 概述 本测试覆盖 Go 运行时三色标记垃圾回收的核心原理,包括颜色不变性、混合写屏障、STW 阶段、Pacer 触发机制及 GC 演进历史,共 10 道题(6 选择 + 3 填空 + 1 简答)。 --- ## 一、选择题(6道,由浅入深) > **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景 ### Q1(基础)— 考察定义层面 在 Go 的三色标记算法中,以下哪个描述正确定义了"灰色对象"? A. 已完成扫描且其引用的对象也都是黑色或灰色的对象 B. 被标记为可回收的白色对象 C. 已被发现但其所引用的子节点尚未扫描的对象 D. 仍在根集扫描阶段的原始全局变量 ### Q2(基础)— 考察行为判断 Go GC 中黑色对象的关键不变性约束是什么? A. 黑色对象不能被任何白色对象引用 B. 黑色对象永远不会直接引用白色对象 C. 所有白色对象必须在下一轮 GC 中被回收 D. 黑色对象只能指向其他黑色对象 ### Q3(进阶)— 理解写屏障的作用 为什么 Go 需要在并发标记期间引入写屏障? A. 为了提高指针赋值的执行速度 B. 为了在标记和清除交错进行时防止可达对象被误回收 C. 为了减少 STW 阶段的根集标记时间 D. 为了让 Pacer 能够更精确地估算堆增长速率 ### Q4(进阶)— 比较辨析 Go 的混合写屏障同时包含白色前置和白色后置两种逻辑,二者的分工是: A. 白色前置保护旧值不被漏扫,白色后置保护新值被纳入扫描 B. 白色前置用于标记阶段,白色后置用于清除阶段 C. 白色前置处理栈上的指针,白色后置处理堆上的指针 D. 白色前置降低 STW 时间,白色后置增加吞吐 ### Q5(深入)— 场景推理 一个服务的 `GOGC=100`,当前 `heap_live = 80MB`。假设这轮 GC 结束后 `heap_live` 仍为 80MB,那么下一轮 GC 将在 `heap_live` 达到多少时被触发?(基于默认触发条件计算) A. 约 115.2MB(80 x 1.44) B. 约 160MB(80 x 2.0) C. 约 88MB(80 x 1.1) D. 约 352MB(80 x 4.4) ### Q6(深入)— 源码级/边界场景 关于 Go GC 的演进历史,下列哪项描述是正确的? A. v1.5 引入混合写屏障,首次实现用户代码与 GC 并行 B. v1.8 引入白色后置写屏障以支持增量标记 C. v1.9 完善了混合写屏障并改进了 Pacer,使终止 STW 缩减至微秒级 D. v1.5 到 v1.8 之间没有使用任何写屏障机制 --- ## 二、填空题(3道) ### F1 — 填空1 在正常扫描流程中,每个灰色对象被取出后遍历其内存中的指针字段。如果某个指针对象当前是白色的,将其标为 \_\_\_\_\_\_ 并入队;扫描完所有子节点后,自身标记为 \_\_\_\_\_\_。 > **提示**: 回想灰色对象的处理循环——先处理子节点再更新自身状态。 ### F2 — 填空2 Go Pacer 的触发阈值基于 `heap_live` 的增长比例。默认情况下,当 `heap_live` 相较于上一轮 GC 结束时增长约 \_\_\_\_\_\_%(即乘数 e^0.37 ≈ \_\_\_\_\_\_)时触发一轮新的 GC。 > **提示**: 这个乘数来源于 GCCycleTargetRatio 参数导出的指数增长公式。 ### F3 — 填空3 要主动触发一次完整 GC 并使用 `runtime.MemStats` 读取堆分配字节数,需要依次调用 `runtime.GC()` 和 \_\_\_\_\_\_。其中 `MemStats` 结构体中表示当前堆已分配字节数的字段名为 \_\_\_\_\_\_。 > **提示**: 参考文档中"主动触发 GC"的代码示例片段。 --- ## 三、简答题(1道) ### S1 某高吞吐 API 服务近期出现延迟抖动,监控显示每次 GC 触发时都会有 1~3ms 的停顿。请结合三色标记 GC 的工作原理,回答以下问题: 1. 哪些阶段会导致 STW 停顿?每个阶段的大致时长是多少? 2. 混合写屏障相比纯前置或纯后置屏障有什么优势?代价是什么? 3. 作为优化建议,可以从哪三个维度降低该服务的 GC 压力? > **答题框架提示**: 先从 STW 阶段入手列出各阶段来源,再对比写屏障方案,最后从代码层面的 GC 友好实践角度给出建议。 --- ## 参考答案与解析 ### 选择题答案 | 题号 | 正确答案 | 解析 | |------|---------|------| | Q1 | C | A 描述的是黑色对象,B 混淆了白色和灰色,D 描述的是根集中尚未着色的变量。灰色对象的定义就是"已发现但子节点未扫描"。 | | Q2 | B | 核心不变性是"黑色对象永远不会直接引用白色对象"。这保证了若一个对象被黑色对象可达,它不可能是白色(会被误回收)。A 方向反了,C 不对——白色对象可能因被黑色对象间接引用而存活,D 太严格——黑色可以指向灰色。 | | Q3 | B | 并发标记期间 Mutator 可能修改引用关系,如果没有写屏障,原本可达的灰色对象可能被取消引用变为白色并被回收。写屏障正是为了保证引用一致性。A 错误——写屏障反而增加了赋值开销。C 和 D 不是写屏障的直接目的。 | | Q4 | A | 白色前置保证旧值(被取消引用的)白色对象不会漏扫;白色后置保证新引用的白色对象会被纳入扫描。两者组合覆盖双向变化。B/C/D 都是对两种屏障功能的曲解。 | | Q5 | A | 默认触发条件是 heap_live 增长 44%,乘数为 1.44(e^0.37)。80 x 1.44 = 115.2MB。选项 B 对应 GOGC=200,C 对应 GOGC=10 的保守模式,D 远超过合理范围。 | | Q6 | C | A 错在 v1.5 引入的是三色标记+并发标记而非混合写屏障。B 错——v1.8 引入的是白色前置写屏障(非后置)。C 正确描述了 v1.9 的改进。D 错——v1.5-v1.8 期间有白色前置写屏障,只是不完全。 | ### 填空题答案 | 题号 | 答案 | 解析 | |------|------|------| | F1 | 灰色(gray)、黑色(black) | 灰色对象被取出后,对其每个指针子节点检查颜色——白色变灰色并入队;全部处理完后自身标黑。这是标准的标记流程。 | | F2 | 44%、1.44 | GCCycleTargetRatio 默认 0.1,对应 heap 增长约 44%(e^0.37 约等于 1.44)时触发。这意味着 GC 频率随堆大小自适应。 | | F3 | runtime.ReadMemStats(&stats)、HeapAlloc | runtime.GC() 立即触发 GC;runtime.ReadMemStats 将运行时统计填入 MemStats 结构体;HeapAlloc 字段表示当前堆分配的字节数。手动触发仅适合 benchmark 或调试。 | ### 简答题参考答案 S1:**参考答案要点**: 1. **STW 阶段与时长大致范围**: - 初始 STW:标记根集为灰色,启动后台扫描器(目标 < 1ms) - 终止 STW(混合屏障后 v1.9+):完成最后一批对象标记(约微秒级) - 清除 STW:重置 freed 对象的白色状态(约几毫秒) - 因此 1~3ms 的停顿主要来自清除阶段或旧版本残留。 2. **混合写屏障的优势与代价**: - 优势:纯前置只保护旧值、纯后置只保护新值,混合方案二者兼得,覆盖所有引用变化的情况,确保并发下不漏扫任何可达对象。代价是每个指针存储操作多了两次颜色检查。 - 纯前置无法处理新值的追踪,纯后置会漏掉被取消引用的旧白色对象。 3. **降低 GC 压力的三个维度**: - 预分配已知大小的容器(slice/map),避免扩容产生额外分配 - 复用对象(如 sync.Pool),减少短生命周期对象的创建 - 减少逃逸到堆上的对象(利用编译器逃逸分析,让临时对象留在栈上) **评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点(STW 阶段名称及时长、写屏障对比分析的优劣、三项以上代码层面的优化建议)。 ## 关联笔记 - [[../runtime/Pprof 性能分析指南]] - [[../concurrency/Goroutine 调度模型]]