函数不断创建用户对象,其中一些被全局缓存引用,另一些处理完成后再也没有任何引用。堆是用于保存动态分配对象的内存区域;仅凭“函数返回”无法判断堆对象是否仍被其他地方使用。
手工释放
程序员决定何时释放。控制直接,但过早释放会悬空,忘记释放会泄漏。
追踪式 GC
从确定仍在使用的根出发沿指针寻找对象,未被找到的内存才能回收。安全性更高,但会消耗 CPU 和额外内存。
GC(Garbage Collection,垃圾回收)是运行时寻找程序已经无法再访问的堆对象,并把它们占用的内存重新用于分配的机制。Go 1.26 默认启用名为 Green Tea 的新回收器;它改进标记和扫描小对象的局部性与并行扩展,但不改变“从根追踪可达对象”的基本心智模型。
函数不断创建用户对象,其中一些被全局缓存引用,另一些处理完成后再也没有任何引用。堆是用于保存动态分配对象的内存区域;仅凭“函数返回”无法判断堆对象是否仍被其他地方使用。
程序员决定何时释放。控制直接,但过早释放会悬空,忘记释放会泄漏。
从确定仍在使用的根出发沿指针寻找对象,未被找到的内存才能回收。安全性更高,但会消耗 CPU 和额外内存。
Root(根)是 GC 明确知道仍在使用的起点,例如活跃 goroutine 栈上的指针和全局变量。Reachable(可达)表示从根沿一个或多个指针能够找到对象。Slice、Map、Channel、字符串和接口值等内部也可能携带需要扫描的地址。
Mark-Sweep(标记-清扫)先标记可达对象,再让未标记空间可重新分配。Go 尽量让标记与应用并发进行,但周期边界仍需要短暂 STW(Stop The World,暂停所有用户 goroutine)完成关键状态切换。
三色标记是一种教学模型:白色表示尚未发现,灰色表示已经发现但它指向的对象还没扫描完,黑色表示自身及其指针都扫描完成。并发期间应用仍会改指针,若黑对象突然指向白对象又没有记录,白对象可能被错误回收。
Write Barrier(写屏障)是在某些指针写入发生时执行的一小段 runtime 逻辑,用来记录需要继续标记的关系,维持回收器要求的不变量。它增加写入成本,但换来并发标记的正确性。
| 颜色 | 当前含义 | 下一步 |
|---|---|---|
| 白 | 本轮还没证明可达 | 若最终仍白,可清扫 |
| 灰 | 已发现,子指针待扫描 | 扫描后变黑 |
| 黑 | 已发现且子指针已扫描 | 本轮无需再扫描,除非屏障记录变化 |
Green Tea GC 在 Go 1.25 作为实验出现,Go 1.26 默认启用。它把更多标记工作围绕更大的内存块组织,改善大量小对象扫描的空间局部性和多核扩展;在支持的平台上还能使用向量指令帮助扫描。
把标记想成每发现一个小对象就随机跳到另一个位置,容易造成缓存未命中和共享工作队列协调。
尽量以页级工作为单位组织扫描,让同一片内存的元数据和对象更可能被连续处理。
GOGC 控制相对上一轮存活堆与根扫描规模的增长目标。MiB(Mebibyte,二进制兆字节)等于 1,048,576 字节。用便于心算的近似例子:上一轮存活堆 100 MiB、需扫描根约 20 MiB、GOGC=100,则目标总堆约为 100 + (100+20)×100% = 220 MiB;真实回收节奏会动态调整。
GOMEMLIMIT 或 debug.SetMemoryLimit 给 runtime 一个软内存目标。它能在容器内存有限时提供更直接的约束,但应为 Go 之外的内存、内核页缓存和突发保留余量。
Go 中常说的“泄漏”通常是对象仍可达,但业务已经不需要。GC 不会猜业务意图,只要根路径存在就不能回收。
| 来源 | 为什么仍可达或仍在工作 | 处理 |
|---|---|---|
| 无上限 Map 缓存 | 全局 map 一直引用对象 | 容量、TTL、淘汰和指标 |
| goroutine 泄漏 | 栈和等待对象仍是根的一部分 | Context、关闭协议、pprof |
| 子 Slice 留大数组 | 小窗口仍指向整个底层数组 | 需要时复制有效部分 |
| 周期任务没有退出协议 | 业务 goroutine 或回调仍主动持有状态并持续运行 | 停止 Ticker,并让执行者响应取消信号 |
| 队列积压 | 业务仍把消息视为待处理 | 限流、背压、容量告警 |
Stop,也可以由 GC 回收。调用 Stop 的主要目的,是明确停止仍在使用的计时行为,而不是帮助 GC 识别一个已经不可达的计时器。runtime.GC() 可能让图表暂时下降,却没有解除保留路径,还增加 CPU 与停顿。先修生命周期,再调回收频率。func main() {
debug.SetGCPercent(100)
debug.SetMemoryLimit(512 << 20) // 512 MiB 软限制
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
work()
runtime.GC() // 仅用于实验观察,不是日常治理手段
runtime.ReadMemStats(&after)
fmt.Println("heap", before.HeapAlloc, "→", after.HeapAlloc)
}
高吞吐 API、批处理、缓存和大量小对象服务需要理解分配率、存活堆与 GC CPU 的关系。多数业务先减少无意义分配、复用缓冲并修复保留路径,再考虑 GOGC;不要为了“零 GC”牺牲可读性和安全。