# Golang 垃圾回收机制 🗑️
Go 语言的垃圾回收器经历了多次演进 📈,从最初的清除标记法到现在的三色标记法配合混合写屏障,每一次升级都让 GC 变得更加高效 ⚡️。让我们一起来了解这个演进过程吧!💪
# 普通清除标记法 (v1.3 以前) ⏪
清除标记法是最基础的垃圾回收算法 🌱,其核心思想就是标记所有需要回收的对象,然后清除这些对象。
# 具体步骤 🔄
- 🛑 暂停程序业务逻辑(STW,Stop The World),找出不可达的对象和可达的对象
- 🏷️ 开始标记,程序找出它所有可达的对象,并做上标记
- 📍 从根对象(全局变量、栈变量等)开始遍历
- 🔍 沿着对象的引用关系进行遍历
- ✅ 将所有可达对象标记为 "使用中"
- 🧹 标记完成后,开始清除未标记的对象
- 🔎 扫描整个堆内存
- ♻️ 回收所有未被标记的对象
- 📦 将回收的内存加入空闲链表
- ▶️ 停止暂停,让程序继续运行。然后循环重复整个过程,直到程序生命周期结束
# 缺点 ⚠️
- ⏸️ STW 导致程序响应延迟
- 🐌 标记需要扫描整个 heap,效率低下
- 🧩 清除数据会导致 heap 碎片
- 📉 整体性能较差
# 三色标记法 (v1.5 以后) 🎨
为了解决清除标记法的问题 🔧,Go 团队引入了更高效的三色标记法。该算法将对象分为三种颜色 🎯:
- ⚪ 白色:对象尚未被标记 (未探索区域)
- 🔘 灰色:对象已经被标记,但它的对象引用尚未被标记 (正在探索)
- ⚫ 黑色:对象已经被标记,且它的对象引用已经被标记 (探索完成)
# 具体步骤 📝
🎬 初始化,所有对象被标记为白色
🚀 GC 开始,遍历 rootset,将直接可达的对象标记为灰色
🔄 遍历灰色对象,将直接可达对象标记为灰色,并将自身标记为黑色
🔁 重复第 3 步,直到标记完所有的对象
🧹 将标记为白色的对象当做垃圾回收掉
# 并发标记的问题 ⚠️
如果三色标记法在并发情况下不被 STW 保护 🛡️,当以下两个条件同时满足时,就会出现对象丢失现象 😱:
- 🔗 条件 1:一个白色对象被黑色对象引用(白色被挂在黑色下)
- ❌ 条件 2:灰色对象与该白色对象之间的可达关系遭到破坏(灰色同时丢失该白色)
# 解决方案 💡
只要破坏上述两个条件中的任意一个即可 🎯:
💪 强三色不变式
- 📜 规则:不允许黑色对象引用白色对象
- 🎯 作用:破坏条件 1,确保黑色对象不会引用白色对象
🤝 弱三色不变式
- 📜 规则:黑色对象可以引用白色对象,但白色对象必须存在灰色对象的间接引用
- 🎯 作用:破坏条件 2,保证白色对象始终可达
# 屏障机制 🛡️
为了实现上述两种不变式,Go 语言引入了两种写屏障机制 🔒:
# 插入写屏障 ➕
- 📝 规则:当一个对象引用另一个对象时,将被引用对象标记为灰色
- ⚡️ 注意:栈内引用的对象不需要写屏障保护
- ⚠️ 缺点:需要在标记结束后对栈进行 STW 和重新扫描,以防止栈上新增对象引用了白色对象
# 删除写屏障 ➖
- 📝 规则:当一个对象引用被删除时,将被删除的对象标记为灰色
- ⚡️ 注意:开始时需要进行 STW,存储快照
- ⚠️ 缺点:被删除的对象即使无引用也会被保留到下一轮,导致回收精度降低
# 混合写屏障机制(hybrid write barrier)🔄
为了综合两种写屏障的优点 🎯,Go1.8 引入了混合写屏障机制:
# 具体策略 📋
- 🎯 GC 开始时将栈上的对象全部标记为黑色(无需后续重新扫描)
- ✨ GC 期间,栈上新创建的对象均为黑色
- 🔄 堆上被删除的对象标记为灰色
- ➕ 堆上新添加的对象标记为灰色
特点:实现了变形的弱三色不变式 🎯,结合了插入、删除写屏障的优点,大大提升了 GC 效率 🚀
##GC 调优
通过 go tool pprof 和 go tool trace 分析 GC 情况,根据实际情况调整 GC 参数
- 控制内存分配的速度,限制 Goroutine 的数量,从而提高
- 减少并复用内存,例如使用 sysc.Pool 来复用需要频繁创建和销毁的对象,例如提前分配足够的内存来降低多余的拷贝
- 需要时,增大 GOGC 的值,降低 GC 的运行效率
# 总结
Go V1.3 普通的标记清除法,整体过程需要 STW,效率低下
Go V1.5 三色标记法,堆空间启用写屏障,栈空间不启用,全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通
Go V1.8 三色标记法,混合写屏障机制,栈空间不启动,堆空间启动,整体过程几乎不需要 STW,效率较高