Go 垃圾回收
区分规范不规定具体 GC 算法与当前实现的并发标记清扫;理解可达性、分配压力、GOGC/GOMEMLIMIT 与工程降分配策略。
[!info] 关联笔记
Go 垃圾回收
这个概念为什么会出现
手动 malloc/free 把生命周期错误变成 use-after-free 与泄漏两类日常事故。Go 选择自动内存管理:堆上不再可达的对象由运行时回收。
但这不意味着“内存免费”:
- 分配本身有 CPU 与缓存成本
- GC 要扫描/标记可达对象,带来 CPU 与偶尔的延迟毛刺
- 错误地长时间持有指针 = 逻辑泄漏
服务性能问题经常落在分配速率、存活集合大小、缓存策略,而不是某个微观语法。
[!abstract] 一句话理解 GC 自动回收堆上不可达对象以降低手动释放负担;你仍需管理分配速率与对象寿命。具体算法与调参是实现细节,可达性语义才是可依赖的心智模型。
最小可运行示例
先把示例放进可观察实验,再看代码:
场景:观测「图片缩略图临时缓冲」制造的分配与回收
图片服务为每次缩略请求 make 约 1MiB 的短命缓冲,用完即丢。
排障时想确认:高峰分配后,强制一次 GC,HeapAlloc / NumGC 是否按「可达性」方向变化——这是可观察实验,不是去证明某个绝对堆公式。
package main
import (
"fmt"
"runtime"
)
// allocThumbBuffers 模拟 N 次缩略请求各自申请 1MiB 临时缓冲。
//
// 业务意图:请求级短命大块,用完不挂到全局。
// 教学点:无引用后对象可被回收;本函数返回后切片不可达。
func allocThumbBuffers() {
for i := 0; i < 1000; i++ {
// 1MiB;是否一定上堆由编译逃逸分析决定(实现细节)。
buf := make([]byte, 1<<20)
// 假装写入了缩略像素后丢弃:不把 buf 存进全局/闭包。
_ = buf[0]
}
}
func main() {
var before, after runtime.MemStats
// 观察窗口 1:压测分配前的堆占用与已发生 GC 次数。
runtime.ReadMemStats(&before)
allocThumbBuffers()
// 仅实验:强制完成一轮 GC。生产服务不要用 GC() 当吞吐旋钮。
runtime.GC()
// 观察窗口 2:回收后对比 HeapAlloc / NumGC。
runtime.ReadMemStats(&after)
fmt.Printf("HeapAlloc before=%d after=%d\n", before.HeapAlloc, after.HeapAlloc)
fmt.Printf("NumGC %d -> %d\n", before.NumGC, after.NumGC)
}
建议运行:
go run .
期望输出形态(具体数字随机器/实现波动,关注相对关系):
HeapAlloc before=... after=...
NumGC N -> N+1 或更大
解读:NumGC 应前进;after.HeapAlloc 通常不再持有那 1000 块仍可达的 1MiB 视图(若你把切片存进全局,after 会明显抬高——可自行改一版对照)。
结合场景再看三个关注点
-
可达才存活
缩略缓冲若不挂全局,GC 后不应再被业务逻辑引用;逻辑泄漏来自「以为扔了其实还被 map/全局钉住」。 -
runtime.GC是诊断探针
可在实验里对齐观测点;线上应靠分配速率与GOGC/GOMEMLIMIT等策略,而不是请求里手动 GC。 -
MemStats要配合 pprof
这里只证明「能观察」;谁在分配、谁在持有,仍看 alloc/inuse。
核心概念与准确模型
规范说了什么 / 没说什么
| 层面 | 内容 |
|---|---|
| 语言/可依赖语义 | 有垃圾回收;程序员通常不显式释放;对象在仍被引用时可认为存活 |
| 未在规范钉死 | 具体 GC 算法、分代与否、触发阈值精确公式、STW 上限绝对保证 |
| 当前实现(gc 工具链,会变) | 非分代、并发标记清扫(concurrent mark-sweep)为主的演进实现;混合写屏障等 |
写笔记与调优时必须口头禅:算法细节 = 实现;调参与版本相关;用观测验证。
可达性
从一组根(栈、全局、寄存器等,实现定义)出发,沿指针可到达的对象视为存活;不可达对象可被回收。
根 → 对象 A → 对象 B
↘ 对象 C(若无其他引用,C 不可达则可回收)
逻辑泄漏:map/slice/全局缓存仍引用“以为扔掉”的数据 → GC 无法收。
分配与压力
GC 频率与成本 Roughly 随:
- 分配速率(单位时间新对象)
- 存活堆大小(扫描工作量)
优化顺序通常是:
- 少分配(复用缓冲、原地处理、避免不必要拷贝)
- 缩短不必要存活(别钉住大底层数组,见 go-slices)
- 再谈
GOGC/GOMEMLIMIT等旋钮
常见实现向旋钮(非规范)
| 旋钮 | 直觉 |
|---|---|
GOGC | 相对目标:堆相对存活集增长百分比触发;off 关闭(危险) |
GOMEMLIMIT | 软内存上限,协助在限制下更积极回收(Go 1.19+ 叙事) |
runtime/debug.SetGCPercent | 程序内调 GOGC |
runtime/debug.SetMemoryLimit | 程序内内存限制 |
具体默认值与交互以当前版本文档为准。
与 pprof 的关系
alloc_space/alloc_objects:谁在分配inuse_space:谁在持有- CPU profile:GC 标记是否显著
见 go-pprof。
边界情况与反直觉行为
1. 有 GC 仍会 OOM
分配快过回收、存活集过大、cgo 外部内存、goroutine 泄漏持有栈/堆引用。
2. 子切片/子字符串钉死大底层
短生命周期视图挂到长生命周期变量 → 整块底层数组存活。
3. finalizer 不是析构函数
runtime.SetFinalizer 执行时机不确定,不能当及时 Close。资源用 defer Close。
4. 强制 GC 的错觉
循环里 runtime.GC() “降内存”往往增延迟、掩问题。
5. sync.Pool
复用减轻分配,但对象可能随时被回收;逻辑必须正确处理“拿不到复用”的路径。属库语义 + 实现协作。
常见误区
[!warning] 常见误区:背诵某版本 GC 论文细节当语言保证 错误:假设永远非分代、某固定 STW 上限写进业务 SLA。
正确:依赖可达性与观测;SLA 用测试与监控证明。
[!warning] 常见误区:内存涨了就怪 GC 有 bug 错误:不查引用链。
正确:heap profile 找持有者。
[!warning] 常见误区:过早 micro-optimize 每个 alloc 错误:未 profile 先对象池化一切。
正确:热点证明后再改。
[!warning] 常见误区:GOGC=off 当生产默认 错误:关掉 GC 冲吞吐。
正确:几乎总是错;用 limit 与降分配。
工程实践
- 指标:
go_memstats_*、容器 RSS、GC 暂停相关 metric(按运行时导出)。 - 降分配:预分配 slice cap、流式处理、避免
[]byte↔string热路径来回。 - 缓存有上限:LRU/TTL,防逻辑泄漏。
- API 生命周期:文档说明是否保留调用方 buffer 引用。
- 负载下验证:bench + pprof + 真实流量。
- 版本升级:GC 实现演进时用回归基准看延迟/内存。
// 独立小切片,避免钉死 big
func clone(b []byte) []byte {
out := make([]byte, len(b))
copy(out, b)
return out
}
可验证实验
实验 1:短命分配
循环 make([]byte, 1<<20) 观察 NumGC 与 CPU。
实验 2:钉住大数组
big := make([]byte, 1<<24); small := big[:16]; runtime.GC() 后经全局保存 small,对比保存 clone(small) 的 heap。
实验 3:GOGC 感性
同程序不同 GOGC=50/200 看 GC 次数与 RSS(实现向实验)。
实验 4:pprof heap
go tool pprof 看 inuse_space 头部。
本节总结
- 可依赖:自动回收不可达堆对象;持有引用则不收。
- 实现向:并发标记清扫等细节、GOGC 行为随版本变。
- 工程核心:分配速率、存活集、泄漏式引用、观测驱动。
- 下一步:并发下“何时看见写入”见 go-memory-model——与 GC 是不同问题。
自测题
概念题
- 规范保证具体 GC 算法吗?
- 什么是逻辑泄漏?
- 为何 finalizer 不能替代 Close?
代码推理题
var cache [][]byte
func handler(buf []byte) {
cache = append(cache, buf[:10])
}
长期运行的风险?
工程思考题
服务 RSS 持续爬升但 HeapAlloc 波动正常。还可能查什么?
参考答案
展开
- 不保证;算法属实现。
- 仍被引用以致不能回收的“无用”数据。
- 时机不确定,不能做及时资源释放。
代码题:buf[:10]可能钉死整个传入 buffer 底层数组;cache 无限增长。应 copy 并限容。
工程题:goroutine 泄漏、cgo/堆外、OS 线程、映射文件、未归还的进程级资源;用合适 profile 与runtime.NumGoroutine等。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| A Guide to the Go Garbage Collector | 官方文档 | 调优与心智(实现向权威入口) |
| GOGC / memory limit 相关 release notes | 发行说明 | GOMEMLIMIT 等(按版本核对) |
| Package runtime | 标准库 | MemStats、GC |
| Package runtime/debug | 标准库 | SetGCPercent、SetMemoryLimit |
| Profiling Go Programs | 博客 | 观测 |
笔记元信息
- 建议文件名:
go-garbage-collection.md - 所属阶段:阶段六
- 学习顺序:性能与运行时
- 建议下一篇:内存模型
- 本篇状态:已深化(明确规范 vs 实现;工程分配策略)