参考文章:
- https://juejin.cn/post/7111515970669117447
- https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
- https://colobu.com/2022/07/16/A-Guide-to-the-Go-Garbage-Collector/
- https://liangyaopei.github.io/2021/01/02/golang-gc-intro/
- https://xargin.com/impl-of-go-gc/
什么是GC?
Go语言的垃圾回收机制Garbage Collection,简称 GC
在传统编程语言中我们需要关注对象的分配位置,要自己去选择对象分配在堆还是栈上,
但在 Go 这门有 GC 的语言中,集成了逃逸分析功能,帮助我们自动判断对象应该在堆上还是栈上,可以使用 go build -gcflags="-m"
来观察逃逸分析的结果
GC的原理是什么?
Go语言的垃圾回收器采用了并发三色标记清除算法(Concurrent Tri-Color Mark-And-Sweep),尽可能减少STW(stop the world)时间,
以降低吞吐为代价换取低延迟,实现了高效的垃圾回收。
标记清除算法的基本原理是,垃圾回收器将所有的存活对象标记为“活”的,未被标记的对象则被认为是垃圾。经典的标记清除算法通常分为两个阶段:
- 标记阶段:垃圾回收器从根对象开始,遍历所有可达对象,并将它们标记为“活”的。
- 清除阶段:垃圾回收器从堆的起始地址开始遍历,将未被标记的对象清除,回收内存。
Go语言的垃圾回收器采用了三色标记法(Tri-Color Marking),将堆上的内存对象分为三种颜色
- 白色:未被标记为“活”的对象,是潜在的垃圾,后续可能会被GC回收。
- 灰色:待扫描的对象,当扫描某个灰色对象时,GC会将其标记为黑色,然后将该对象指向的所有对象都标记为灰色,待后续标记。
- 黑色:被标记为“活”的对象,在这轮GC中不会被回收。
垃圾回收器开始工作时不存在黑色对象,垃圾回收器会将根对象标记为灰色,并从根对象(通常是栈对象和全局对象)开始遍历。垃圾回收器会将灰色对象标记为黑色,
并将该对象指向的对象标记为灰色。垃圾回收器重复这个过程,直到所有可达对象都被标记为黑色。最后,垃圾回收器清除所有未被标记为黑色的对象,即清除所有白色对象。
GC如何优化?
优化GC的开销是提高系统性能和响应速度的重要手段。
缩短STW时间
Go GC触发时机大体分为三种:
- 手动触发:调用runtime.GC()
- 常规触发:Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100
- sysmon后台周期性强制触发GC
减少堆内存的分配和释放
GC开销大的根源在于heap object多,Go的每轮GC都是FullGC,每轮都要将所有heap object标记(mark)一遍,
即便大多数heap object都是长期alive的,因此,一个直观的降低GC开销的方法就是减少heap object的数量,即减少alloc。
- 把小对象聚合到一个结构体中,然后做一次分配即可
- 内存空间重用,如:sync.Pool
常见的GC
- 引用计数法
根据对象自身的引用计数来回收,当引用计数归零时进行回收,但是计数频繁更新会带来更多开销,且无法解决循环引用的问题。- 优点:简单直接,回收速度快
- 缺点:需要额外的空间存放计数,无法处理循环引用的情况;
- 标记清除法
标记出所有不需要回收的对象,在标记完成后统一回收掉所有未被标记的对象。- 优点:简单直接,速度快,适合可回收对象不多的场景
- 缺点:会造成不连续的内存空间(内存碎片),导致有大的对象创建的时候,明明内存中总内存是够的,但是空间不是连续的造成对象无法分配;
- 复制法
复制法将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉- 优点:解决了内存碎片的问题,每次清除针对的都是整块内存,但是因为移动对象需要耗费时间,效率低于标记清除法;
- 缺点:有部分内存总是利用不到,资源浪费,移动存活对象比较耗时,并且如果存活对象较多的时候,需要担保机制确保复制区有足够的空间可完成复制;
- 标记整理
标记过程同标记清除法,结束后将存活对象压缩至一端,然后清除边界外的内容- 优点:解决了内存碎片的问题,也不像标记复制法那样需要担保机制,存活对象较多的场景也使适用;
- 缺点:性能低,因为在移动对象的时候不仅需要移动对象还要维护对象的引用地址,可能需要对内存经过几次扫描才能完成;
- 分代式
将对象根据存活时间的长短进行分类,存活时间小于某个值的为年轻代,存活时间大于某个值的为老年代,永远不会参与回收的对象为永久代。
并根据分代假设(如果一个对象存活时间不长则倾向于被回收,如果一个对象已经存活很长时间则倾向于存活更长时间)对对象进行回收。