Go的GC只基于可达性判断对象是否存活,不关心变量是否为指针类型;根对象包括全局变量、goroutine栈上变量等,只要能从根通过引用链到达即视为可达,否则回收。

Go 的 GC 不看指针,只看可达性
Go 的垃圾回收器从不检查某个变量是不是 *T 类型、有没有被声明为指针——它只关心“从根对象出发,能不能顺着引用链访问到这个对象”。哪怕你把一个 *int 赋值给 interface{} 或放进 map[string]interface{},只要它还能被程序逻辑访问到,就不会被回收。
常见错误现象:runtime.GC() 后内存没降,或 pprof 显示某段内存长期驻留,结果发现只是某个闭包意外捕获了大对象的指针,或者 goroutine 持有对它的引用却没退出。
- 根对象包括:全局变量、当前 goroutine 栈上的局部变量(含形参)、寄存器中存活的指针、正在运行的 goroutine 的栈帧
- 只要一个对象能通过任意一条指针路径(不管嵌套多深、经过多少层
struct字段或slice元素)抵达,它就算“可达” -
unsafe.Pointer和反射操作(如reflect.Value.Addr())生成的指针,GC 也能识别——但绕过类型系统的裸地址(如用uintptr存地址)会被 GC 当作普通整数忽略,导致悬垂指针和提前回收
哪些情况会让对象“意外存活”?
不是指针用法错了,而是引用关系没断干净。最典型的是“本该短命的对象被长生命周期结构间接持有”。
- 向全局
sync.Pool放入带指针字段的 struct,但没清空字段;下次 Get() 可能拿到残留引用 - 用
slice的[:]截取时底层数组未释放,比如largeSlice[:10]返回小 slice,但底层仍占大内存,且 GC 会因该 slice 可达而保留整个底层数组 - goroutine 泄漏:启动一个 goroutine 处理 channel,但 sender 已关闭或忘记 close,receiver 卡在
range或<-ch,它栈上持有的所有局部变量(包括指向大对象的*T)全无法回收 - 闭包捕获外部变量:函数返回一个闭包,闭包内用了外部
data *HeavyStruct,即使原函数已返回,只要闭包还活着,data就活著
如何验证某个对象是否被 GC 回收?
不能靠 fmt.Println 或日志——它们本身可能引入引用。真要确认,得用 runtime.SetFinalizer 配合手动触发 GC 观察回调是否执行。
立即学习“go语言免费学习笔记(深入)”;
示例:
obj := &struct{ x [1<<20]byte }{}
runtime.SetFinalizer(obj, func(_ interface{}) { fmt.Println("collected") })
obj = nil // 断开引用
runtime.GC()
time.Sleep(time.Millisecond) // 确保 finalizer queue 被处理
- finalizer 不保证一定执行,也不保证何时执行;仅用于调试和资源泄漏排查,绝不能用于关键清理逻辑
- 必须确保对象没有其他引用(包括 global map、channel、未关闭的 goroutine 栈),否则 finalizer 永远不会调
-
pprof heap中看inuse_space和allocs_space对比,再结合go tool pprof --alloc_space定位分配源头,比猜更可靠
指针不是 GC 的开关,逃逸分析才是关键
写 *T 不等于对象一定堆分配,也不等于 GC 一定会管它——Go 编译器会做逃逸分析,决定变量放栈还是堆。栈上对象生命周期由函数调用控制,根本进不了 GC 视野。
- 如果
new(T)或&T{}的结果被返回、传给接口、存入切片/映射、或地址被赋给全局变量,就会逃逸到堆 - 反之,像
var x T; px := &x且px只在函数内使用,通常不逃逸,x是栈对象,函数返回即销毁 - 用
go build -gcflags="-m"查看逃逸分析结果,比盲目加指针更有效
真正难缠的从来不是“怎么写指针”,而是搞清谁在什么时候持有了它的引用——GC 只认路径,不认语法。










