Easton Notes
八股Go 面试题

垃圾回收

Go GC 的三色标记、写屏障、触发条件、观测与调优。

常见的垃圾回收算法有哪些?

回答重点

常见思路包括:

  • 引用计数:记录对象引用数量,归零后回收;难点是循环引用和计数维护成本。
  • 追踪式 GC:从根集合出发找到所有可达对象,未被找到的对象可回收。
  • 标记清扫:标记可达对象,再回收未标记对象。
  • 标记整理或复制:在回收时移动对象,减少碎片,但需要更新引用。
  • 分代 GC:根据“多数对象朝生夕死”的假设,对不同年龄对象采用不同回收频率。
  • 增量或并发 GC:把工作拆分,或让部分回收过程与用户代码并发执行,降低长停顿。

实际垃圾回收器通常组合多种技术,而不是只属于一个标签。

Go 使用什么垃圾回收器?

回答重点

标准 Go 工具链使用并发的、追踪式、非移动、非分代标记清扫 GC。标记工作大部分与用户代码并发执行,运行时使用写屏障维持并发标记的正确性。

“非移动”意味着当前实现不会为了整理堆而搬迁普通对象;“非分代”表示不会按对象年龄划分年轻代和老年代。它们是标准工具链的实现特征,不是 Go 语言规范承诺。

三色标记法是什么?

回答重点

三种颜色描述的是标记状态:

  • 白色:尚未确认可达。
  • 灰色:已经确认可达,但其引用还未扫描完成。
  • 黑色:已经确认可达,且其引用已经扫描完成。

GC 从根集合开始,把可达对象加入灰色工作队列;扫描灰色对象时,将它引用的白色对象变灰,扫描完成后自身变黑。标记结束时仍为白色的对象不可达,可以在清扫阶段回收。

颜色是算法模型,不是对象里一定存在一个名为 color 的字段。

GC 的根对象有哪些?

回答重点

根集合是无需通过其他堆对象就能直接找到的指针来源,主要包括:

  • goroutine 栈上的指针。
  • 全局变量中的指针。
  • 运行时维护的特定根,例如 finalizer 或清理机制相关状态。

GC 从这些根出发遍历对象图。寄存器中的活动值会在安全点与栈扫描机制配合处理,具体细节依赖运行时和架构。

什么是 STW?

回答重点

STW 是 Stop The World,表示运行时暂停普通 Go 代码执行,以完成需要全局一致性的 GC 工作。

Go 的并发 GC 仍有短暂 STW 阶段,例如标记开始和标记终止。优化目标是减少停顿时间和频率,而不是宣称“完全没有 STW”。

并发标记为什么需要写屏障?

回答重点

GC 标记期间,用户代码仍可能修改对象引用。如果一个已扫描对象开始指向尚未标记的对象,同时原有可达路径被删除,GC 可能漏标仍在使用的对象。

写屏障是在指针写入附近执行的一小段运行时逻辑,用于记录或着色相关对象,维持三色不变性。它让 GC 在不长时间停止用户代码的情况下继续正确标记。

写屏障只在需要的 GC 阶段启用,并非所有写操作永久承担相同成本。

Go 的混合写屏障解决了什么问题?

回答重点

Go 的混合写屏障结合了删除屏障和插入屏障的思想:

  • 覆盖旧指针时,保守地保留旧引用指向对象的可达性。
  • 当前 goroutine 的栈尚未完成扫描时,写入的新指针也需要被保护。

goroutine 栈不使用与堆完全相同的写屏障。混合写屏障配合栈扫描,使运行时不再需要在标记结束时对所有栈执行一次完整重扫。

具体伪代码和条件会随运行时实现演进。面试中应讲清“防漏标、保护旧引用与特定新引用、避免全量重扫”这三个目标,不要把所有新旧对象都无条件染灰。

Go GC 的一个周期有哪些阶段?

回答重点

可以概括为:

  1. 清扫终止:短暂 STW,完成上一轮遗留清扫准备。
  2. 并发标记:开启写屏障,扫描根和堆对象;用户分配过快时会承担 mark assist。
  3. 标记终止:短暂 STW,完成标记并关闭写屏障。
  4. 并发清扫:回收未标记对象占用的内存,使其可再次分配。

把 scavenger 向操作系统归还空闲页的过程单独称为第二个 GCoff 阶段容易误导。它与 GC 周期协作,但有自己的后台和按需工作机制。

GC 在什么时候触发?

回答重点

主要触发来源包括:

  • 堆增长达到 GC pacer 计算出的目标。
  • 接近 GOMEMLIMIT 设置的软内存限制。
  • 调用 runtime.GC() 主动触发。
  • 运行时在长时间没有自然 GC 时执行强制周期。

GOGC=100 不是简单的“进程内存翻倍就 GC”。当前模型会根据上一轮存活堆和 GC 根大小计算目标;栈、全局变量、堆外内存以及操作系统看到的 RSS 也不能直接等同于 Go 堆目标。

GOGC 和 GOMEMLIMIT 有什么区别?

回答重点

  • GOGC 控制 GC 的 CPU 与堆空间权衡。值越大,通常 GC 频率越低、额外堆空间越多。
  • GOMEMLIMIT 为运行时管理的总内存设置软限制,包含 Go 堆和其他运行时内存,但不包含映射文件、外部进程内存等所有 RSS 来源。

在容器中通常先根据实际内存配额设置合理的 GOMEMLIMIT,保留原生库、内核页缓存和突发空间,再通过压测调整 GOGC。软限制不是绝对上限,设置过低会导致频繁 GC 甚至吞吐崩溃。

GC 应关注哪些指标?

回答重点

  • 存活堆大小和堆目标。
  • 分配速率与每次请求分配量。
  • GC CPU 占比和 mark assist 时间。
  • STW 停顿分布,而不只是平均值。
  • goroutine 栈与运行时元数据大小。
  • GC 周期频率和进程 RSS。

单个指标不能独立说明问题。例如 RSS 不下降不等于泄漏,可能只是运行时保留了空闲页用于后续分配。

有 GC 为什么仍会出现内存泄漏?

回答重点

GC 只判断对象是否可达,不理解业务上是否还需要它。只要对象仍能从根集合访问,GC 就必须保留。

因此 Go 中常说的“内存泄漏”多数是:

  • 缓存、Map、队列无限增长。
  • goroutine 泄漏导致其引用对象长期存活。
  • 小对象引用大底层数组。
  • 回调、订阅或全局注册表未移除。

此外,还要区分真实存活堆增长、短期分配峰值、内存碎片和空闲页尚未归还操作系统。

如何调优 Go GC?

回答重点

  1. 先减少不必要分配:预分配 slice、复用缓冲区、避免热路径装箱和临时字符串。
  2. 用 heap profile 和 benchmark 找到主要分配点,不凭感觉优化。
  3. 设置符合部署环境的 GOMEMLIMIT,避免容器接近 OOM。
  4. 根据延迟、吞吐和内存预算调整 GOGC。
  5. 关注更改后的尾延迟、GC CPU、存活堆和 RSS,避免只优化一个数字。

debug.SetGCPercent(500) 不等于“堆达到上次的 500% 才触发”,也不是通用优化答案。

如何观察 Go GC?

回答重点

常用工具:

  • GODEBUG=gctrace=1:查看每轮 GC 的时间、CPU、堆大小和目标。
  • runtime/metrics:以稳定指标名采集运行时数据。
  • runtime.ReadMemStats:读取兼容性较强的内存统计快照。
  • net/http/pprof 与 go tool pprof:分析存活对象、累计分配和 goroutine。
  • runtime/trace 与 go tool trace:分析调度、阻塞和 GC 活动。
GODEBUG=gctrace=1 ./server
go tool pprof http://localhost:6060/debug/pprof/heap
go tool trace trace.out

gctrace 的字段可能随版本变化,解释日志时应对照当前 Go 版本的运行时文档,不要长期保存一份固定字段表。

扩展知识

评论

On this page