返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

3.1 概述

发布于 2024-10-12 19:15:56 字数 4002 浏览 0 评论 0 收藏 0

翻译自 1.18 mgc.go 头部注释。

回收线程(collector)和其他线程(mutator)可并发执行,允许多个回收线程并行。

  • 类型精确(type accurate)
  • 非代龄(non-generational)
  • 非收缩(non-compacting)
  • 写屏障(write barrier)
  • 并发标记(mark)和清理(sweep)

回收周期包含以下几个步骤:

  1. 清理结束
  • STOP-THE-WORLD ,让所有 P 到达安全点。
  • 清理所有未及清理的内存块。(清理未结束前执行 runtime.GC)

  1. 开始标记
  • 状态 GCoff -> GCmark。
  • 启用写屏障。
  • 启用辅助(mutator assists)。
  • 根标记任务排队。
  • 所有 P 写屏障启用前,不扫描任何对象。
  • START-THE-WORLD
  • 调度器(schedule)启动标记工人(mark workers)和分配辅助(assists)。
  • 写屏障遮蔽指针写入(shades both the overwritten pointer and the new pointer value)。
  • 新分配对象(malloc)直接标记为黑色。
  • 执行根标记任务。
  • 扫描所有栈(stack)。(会导致 goroutine 停止,扫描完成后恢复)
  • 遮蔽所有全局变量(global)。
  • 遮蔽堆外运行时(off-heap runtime)数据结构中的堆指针。
  • 清空灰色队列。(drains the work queue of grey objects)
  • 扫描其中的灰色对象(grey object),将其标记为黑色。
  • 对其包含的指针进行着色,将引用目标放入灰色队列。
  • 终止算法(distributed termination algorithm)检测何时不再有根标记任务和灰色对象。
    标记结束(gcMarkDone)。
  1. 标记结束
  • STOP-THE-WORLD
  • 状态 GCmarktermination,禁用标记工人和分配辅助。
  • 处理内部任务(flushing mcaches)。
  1. 执行清理
  • 状态 GCoff,设置清理状态,禁用写屏障。
  • START-THE-WORLD
    从此刻起,新分配对象为白色,必要时会在分配前清理目标内存。
  • 后台执行并发清理(oncurrent sweeping),并响应分配操作。
  1. 分配超过阈值,重新回到第 1 步

并发清理

清理与用户逻辑并发执行,专门的 goroutine 在后台挨个清理。标记结束后,所有 span 都被标记为需要清理。

为避免向 OS 请求过多内存,首先尝试清理现有 span 以获取可复用空间。确保不会在未清理的 span 上执行操作,避免破坏标记位图。回收期间,mcache 所持有的 span 全部收回 mcentral。重新获取时,会执行清理操作。而当下一回收周期启动时,也会先完成未清理任务。

回收速率

环境变量 GOGC 控制了回收和分配间的线性比例。如果 GOGC = 100,使用了 4 MB,那么到达 8 MB 时,垃圾回收将被再次启动。

控制器

控制器(gcController)用于 GC 调控,决定何时触发,有多少工作量,需要多少投入和辅助。基于每个回收周期的堆增长和 CPU 利用率等数据,以反馈算法(feedback control algorithm)进行调整。该算法将辅助标记和后台标记的 CPU 利用率优化为 GOMAXPROCS 的 25%。

垃圾回收有个大问题需要解决,那就是什么时候启动?过早或频繁启动,除浪费 CPU 资源外,还会加大用户逻辑停顿时间。过晚又会导致堆膨胀,浪费更多内存。如何在两者间平衡,是个巨大的挑战。

并发回收阶段,对象分配速度可能远快于回收标记。这会引发一系列恶果,比如堆恶性扩张,或导致单个回收周期无法结束,以至于垃圾回收机制瘫痪。此时,暂停用户逻辑,让该线程临时参与辅助回收就十分必要。此举非但能抑制用户逻辑在短时间内大批量分配内存,还可提升回收效率。平衡分配和回收,更充分复用内存。

写屏障

直观上看,写屏障是编译器在用户逻辑内插入的额外指令。

因三色标记和用户逻辑并发执行,那么已检查的黑色对象就可能被修改。假设已扫描黑色对象内部指针 “突然” 指向一个尚未扫描白色对象。按三色标记流程,黑色不会再次扫描,如此就导致该白色对象最终被回收,从而引发逻辑错误。

A (黑) 引用 B (灰);B 引用 C (白)。

然后,A 引用 C,B 不再引用 C。

如没有写屏障,那么 A 不会再次扫描,C 保持白色被回收。

写屏障启用后,对指针的修改会跳转到写屏障指令,以便对其重新标记、扫描。如此,其引用的白色对象会存活下来。写屏障解决了垃圾回收与用户逻辑并发执行的冲突,有助于减少重新扫描次数,简化和消除了某些复杂机制。

写屏障仅在垃圾回收时启用,通过特定开关进行判断。

正常情况下,用户逻辑不会跳转到这些额外指令,性能不受影响。

三色标记

扫描内存时,使用三种颜色标记对象状态。

  • 起初,所有对象默认为白色。
  • 扫描,可达对象如包含指针,标记为灰色后放入待处理队列,否则直接黑色。
  • 依次从队列提取灰色对象,扫描其指针字段。
  • 该灰色对象标记为黑色,表示存活。
  • 其字段所引用对象,标记为灰色后放回队列。
  • 扫描和队列结束,仅剩黑白二色。
  • 黑色为存活对象。
  • 白色表示待回收空间。

通过队列实现递归扫描,找出存活(黑色)对象,其余被回收。这就是三色标记原理,大体与扫描流程相对应。至于用户逻辑在扫描阶段新分配的用户对象,则直接标记为黑色。

标记操作对于:

a := make([]*int, 1e9)

b := make([]int, 1e9)

性能相差巨大。因为 b 无需深入内部扫描,故而快得多。

所以,对于超大块内存使用要慎重。

比如说,分配一大块 []byte 或 mmap,持有阻止回收。然后,在内部使用 uintptr 二次分配。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文