Go的垃圾回收机制
并发三色标记 + 混合写屏障
并发三色标记:
三色标记:黑白灰;
- 黑色:表示对象自身存活,且指向的所有对象均已标记完成。
- 灰色:表示对象自身存活,但指向对象还没有被标记完成。
- 白色:表示对象可能是垃圾需要回收,对象没有被标记。
标记过程:
- 首先将根对象变
黑
,并将其指向的对象变灰
。 - 接下来开始多轮染色,将当前
灰
对象自身变黑
,它们指向的所有对象变灰
。 - 重复多轮,直到没有对象可以被染色了,结束标记,并清除
白
对象。
GC过程并不会阻塞用户的goroutine运行;
三色标记是标记清扫算法的衍生版本,对于内存碎片问题,通过Go自身的内存模型解决了外部碎片,降低内部碎片(TCMalloc机制)。
漏标与多标
漏标:某一个时刻,存活对象未被标记到,而被误删。例如,已经变黑
的黑对象A新引用一个白对象C,而原本引用白对象C的灰对象B,在变黑
前删除了对白对象C的引用,此时白对象C仅被黑对象A引用,但GC认为白对象C不可达,会误删白对象C。
多标:某一个时刻,对象没有被引用了,但是被标识过了,延迟一轮GC才能正常回收。例如,灰对象的引用被全部删除,也不能直接褪色,需要等待多一轮GC。
混合写屏障:
主要解决漏标问题。
强三色不变式(插入写屏障):白对象不能被黑对象直接引用。简单来说,A引用B时,B必须被标记为灰色。
缺点:结束时需要暂停(STW),以便重新扫描栈,标记栈上引用的白色对象的存活。
弱三色不变式(删除写屏障):白对象被黑对象直接引用的前提是从某个灰对象出发仍然可达该白对象。如果灰对象尝试删除对白对象的引用,白对象会直接变为灰色,引用再被移除。
缺点:回收精度低,GC开始时STW扫描堆栈来记录初始快照。
混合写屏障:强三色不变式+弱三色不变式:
- GC开始前将栈上所有对象扫描后标记为黑色。
- GC期间,栈上新创建的对象均直接置为黑色。
- 被删除的堆上对象标记为灰色。
- 被添加的堆上对象标记为灰色。
栈对象特点:
- 局部性 & 生命周期短暂:栈对象通常是函数内部声明的局部变量或参数。它们只在函数执行期间存在,并且在函数执行结束时自动销毁。
- 速度:由于栈是线程私有的,而且具有固定大小的分配,所以对栈对象的访问通常比对堆对象的访问速度更快。
- 大小确定:栈对象通常具有固定大小,由编译时或运行时确定。
栈对象(线程栈)被引用时的条件:
- C有对A的引用,即C对B的引用是通过A进行查询并引用的。
- C的栈内存中有其他对象可以达到B。
常见其他GC算法
标记清扫
标记阶段:从根对象出发,可到达的内存会被标记(Mark)。
清扫阶段:所有未被标记的对象都被认为是垃圾对象,即不再被程序引用的对象。
清扫阶段会遍历整个堆,对每个对象进行检查,对于未被标记的对象,垃圾回收器会将其回收,释放其占用的内存空间。
根对象包括:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
- 执行栈:每个
goroutine
都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。 - 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
优点:不会因为移动对象而产生内存碎片。
缺点:在清扫阶段,会产生内存碎片,导致内存利用率降低。需要暂停程序执行直到清扫完成,影响程序的响应性能。
标记压缩
在标记清扫的基础上,清扫的同时,将存活对象压缩到一起,尽可能减少内存碎片。但清扫过程中存在对象拥有大型堆内存,频繁创建消耗对象,投入容易大于产出。
半空间复制
将内存空间一分为二,fromspace
和tospace
。仅使用一半的内存空间进行分配,如一开始使用fromspace
,发生GC时,GC将fromspace
内的存活对象移到tospace
中,清空fromspace
,变相实现压缩内存空间,清除内存碎片。下一轮使用tospace
分配空间,后续操作以此类推。
优点:降低了算法的时间复杂度。
缺点:明显浪费了一半的内存空间。
分代垃圾回收
简单来说将对象按照经历过的GC次数进行划分,一般划分为年轻代和老年代。Go存在内存逃逸机制,即在编译过程中,生命周期长的对象会被分配到堆上,短的会分配到栈上,以栈为单位回收这部分对象。
引用计数
对象被引用则加一,删除引用则减一,GC将回收计数器为0的对象。无法解决循环引用或者自引用问题。