Scavenger 是 Go 1.12+ 引入的独立后台 goroutine,与 GC 解耦:GC 负责对象标记清除,Scavenger 仅负责将空闲≥128 MiB且≥5分钟的内存页归还操作系统。

Go 的 Scavenger 不是 GC 的一部分,它不负责对象回收,只管把空闲的内存页还给操作系统——而且默认要等 5 分钟。
Scavenger 是什么,和 GC 有什么区别
Scavenger(scvg)是 Go 1.12+ 引入的独立后台 goroutine,和 GC 完全解耦:GC 负责标记清除对象,Scavenger 负责扫描 mheap 中已清空的 span,判断是否该调用 madvise(MADV_DONTNEED) 或 munmap 归还物理内存。你看到 GODEBUG=gctrace=1 日志里频繁出现 scvg,说明它在跑,但不代表内存正在释放——它只在满足条件时才动手。
- GC 触发后,对象被回收,对应
span进入“空闲”状态,但仍在 runtime 内存池中待复用 - Scavenger 每次扫描会检查这些空闲
span的空闲时长和大小,只有同时满足:span空闲 ≥ 128 MiB 且空闲时间 ≥scavengelimit(默认 5 分钟)才会真正归还 - 它不阻塞用户代码,但也不会“抢在 GC 前”或“配合 GC 步调”——两者调度完全独立
怎么确认 Scavenger 是否真在工作
别只看 runtime.ReadMemStats().HeapReleased 是否上涨,那只是累计值;得看它当前有没有活跃动作:
- 用
go tool trace启动程序,打开浏览器查看 trace,筛选Scavengergoroutine,观察其执行频率和持续时间 - 在终端运行
cat /proc/<pid>/smaps | awk '/^Rss:/ {sum += $2} END {print sum " kB"}'</pid>,对比MemStats.Sys和HeapSys:若 RSS 高出HeapSys20% 以上,说明大量内存卡在 mmap/cgo 层,Scavenger 根本管不到 - 设置
GODEBUG=madvdontneed=1(Go 1.21+)可让 Scavenger 更激进地释放,但会增加延迟,压测时再开
为什么 runtime.GC() 后 RSS 不降
因为 runtime.GC() 只触发一次 GC 循环,它不唤醒 Scavenger,也不影响 scavengelimit 计时器。常见误解是“GC 完了内存就该没了”,实际流程是:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 分配大 slice → runtime 用
mmap向 OS 拿内存 →HeapSys和 RSS 同步涨 - 变量超出作用域 → GC 标记清除 →
HeapInuse下降,但span仍空闲保留在 heap 中 - Scavenger 还没到 5 分钟阈值,或空闲
span不够 128 MiB →HeapReleased不动,RSS 维持高位 - 你手动调
debug.FreeOSMemory()才会强制扫全堆、立刻归还——但它暂停所有 goroutine,生产环境禁用
生产环境该怎么做,而不是瞎调参数
靠调 GOGC 或 GOMEMLIMIT “催 Scavenger”是徒劳的;真正有效的是控制源头:
- 避免单次分配超 1 MiB 的切片,改用
sync.Pool复用缓冲区(比如bufio.Reader) - 读大文件别用
io.ReadAll,改流式处理 + 固定 buffer - 怀疑 cgo 泄漏?加
CGODEBUG=cgocheck=2运行,或静态编译后用valgrind --tool=memcheck - 监控关键指标组合:
MemStats.HeapReleased(已还给 OS 的量)、MemStats.HeapIdle(当前空闲但未还的量)、/proc/pid/smaps Rss(真实物理占用)
Scavenger 的设计本意就是“懒”——它宁可多留几 MB 内存,也不愿为省这点代价引入系统调用抖动。理解它不干活的时机,比强行让它干活更重要。

















