Daily Study
更新: 1/1/2026 字数: 0 字 时长: 0 分钟
Daily Plan
#todo
- [ ]
Go GC回顾
Go的垃圾回收机制工作原理
Go 的 GC 目前使用的是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户代码并发执行)的三色标记清扫算法。
三色标记法:Go将堆内存中的对象分为三种颜色:
- 白色 :潜在的垃圾。GC 开始时,所有对象都是白色的。如果一轮标记结束后还是白色,说明不可达,会被回收。
- 灰色 :活跃对象,但子对象还没扫描完。它是中间状态,相当于一个“任务队列”。
- 黑色 :活跃对象,且子对象也扫描完了。绝对安全,不会被回收。
工作流程:
- 初始状态:所有对象都是白色的。
- 根节点扫描:从 Root Set(全局变量、栈上的变量)出发,找到它们引用的对象,标记为灰色。
- 循环处理:从灰色集合里拿出一个对象,标记为黑色。然后,把这个对象引用的所有白色子对象,标记为灰色。
- 结束条件:当灰色集合为空时,标记结束。
- 清除:剩下的白色对象就是垃圾,直接清理。
为了解决并发问题,Go采用混合写屏障(1.8版本之后)核心逻辑是:只要你在 GC 期间修改指针,不管是新增引用还是删除引用,屏障都会把涉及到的对象强制染成灰色。
GC的生命周期:
- 清理终止(STW):暂停程序,确保上一轮清理工作彻底完成
- 标记(并发):开启写屏障,GC协程和用户协程一起跑
- 标记终止(STW):关闭写屏障,标记完成
- 清除(并发):程序恢复运行,GC协程并发地把白色内存块回收给分配器
什么时候触发GC
根据堆内存的大小来触发,有一个环境变量GOGC来控制触发频率,默认值100,公式 下次 GC 触发堆大小 = 上次 GC 后的存活堆大小 * (1 + GOGC/100)
什么情况会导致频繁的GC
从内存出发,分析内存大小和内存分配速度,以下是5种典型场景:
- GOGC设置过小
- 瞬间分配大量短命的小对象,解放方法:使用
sync.Pool复用对象 - 逃逸分析失败(写的太动态),使用 go build -gcflags =”-m" 进行逃逸分析
string和[]byte频繁转换,在 Go 中,string是不可变的,而[]byte是可变的。s := string(bytes)或b := []byte(str)默认都会发生内存拷贝(Deep Copy)。在处理网络包、解析 JSON、日志拼接时,频繁进行类型转换。解决方法:使用strings.Builder或bytes.Buffer拼接字符串。- 内存泄露
无分代和不整理的原因
- 对象整理的优势是解决内存碎片问题以及“允许”使用顺序内存分配器。但 Go 运行时的分配算法基于 tcmalloc,基本上没有碎片问题。并且顺序内存分配器在多线程的场景下并不适用。Go 使用的是基于 tcmalloc 的现代内存分配算法,对对象进行整理不会带来实质性的性能提升。
- 分代 GC 依赖分代假设,即 GC 将主要的回收目标放在新创建的对象上(存活时间短,更倾向于被回收),而非频繁检查所有对象。但 Go 的编译器会通过逃逸分析将大部分新生对象存储在栈上(栈直接被回收),只有那些需要长期存在的对象才会被分配到需要进行垃圾回收的堆中。也就是说,分代 GC 回收的那些存活时间短的对象在 Go 中是直接被分配到栈上,当 goroutine 死亡后栈也会被直接回收,不需要 GC 的参与,进而分代假设并没有带来直接优势。并且 Go 的垃圾回收器与用户代码并发执行,使得 STW 的时间与对象的代际、对象的 size 没有关系。Go 团队更关注于如何更好地让 GC 与用户代码并发执行(使用适当的 CPU 来执行垃圾回收),而非减少停顿时间这一单一目标上。
