速通 GO 垃圾回收机制

速通 GO 垃圾回收机制

前言

垃圾回收(Garbage Collection,简称 GC)是编程语言中自动管理内存的一种机制。Go 语言从诞生之初就带有垃圾回收机制,经过多次优化,现在已经相当成熟。本文将带您深入了解 Go 语言的垃圾回收机制。

下面先一起了解下涉及到的垃圾回收相关知识。

标记清除

标记清除(Mark-Sweep)是最基础的垃圾回收算法,分为两个阶段:

  1. 标记阶段:从根对象出发,标记所有可达对象(可达性分析)
  2. 清除阶段:遍历整个堆,回收未被标记的对象
go 复制代码
// 伪代码展示标记清除过程
func MarkSweep() {
    // 标记阶段
    for root := range roots {
        mark(root)
    }
    
    // 清除阶段
    for object := range heap {
        if !marked(object) {
            free(object)
        }
    }
}

标记清除算法的优点是实现简单,但存在以下问题:

  • 需要 STW(Stop The World),即在垃圾回收时需要暂停程序运行
  • 会产生内存碎片,因为清除后最终剩下的活跃对象在堆中的分布是零散不连续的
  • 标记和清除的效率都不高

三色标记

为了优化标记清除算法,Go 语言采用了三色标记算法。主要的目的是为了缩短 STW 的时间,提高程序在垃圾回收过程中响应速度。

三色标记将对象分为三种颜色:

  • 白色:未被标记的对象
  • 灰色:已被标记但其引用对象未被标记的对象
  • 黑色:已被标记且其引用对象都已被标记的对象

在垃圾回收器开始工作时,所有对象都为白色,垃圾回收器会先把所有根对象标记为灰色,然后后续只会从灰色对象集合中取出对象进行处理,把取出的对象标为黑色,并且把该对象引用的对象标灰加入到灰色对象集合中,直到灰色对象集合为空,则表示标记阶段结束了。

详细标志过程如下:

  1. 初始时所有对象都是白色
  2. 从根对象开始,将其标记为灰色
  3. 从灰色对象集合中取出一个对象,将其引用对象标记为灰色,自身标记为黑色
  4. 重复步骤 3 直到灰色集合为空
  5. 清除所有白色对象
go 复制代码
// 三色标记伪代码
func TriColorMark() {
    // 初始化,所有对象设为白色
    for obj := range heap {
        setWhite(obj)
    }
    
    // 根对象入灰色队列
    for root := range roots {
        setGrey(root)
        greyQueue.Push(root)
    }
    
    // 处理灰色队列
    for !greyQueue.Empty() {
        grey := greyQueue.Pop()
        scan(grey)
        setBlack(grey)
    }
    
    // 清除白色对象
    sweep()
}

需要注意的是,三色标记清除算法本身是不支持和用户程序并行执行的,因为在标记过程中,用户程序可能会进行修改对象指针指向等操作,导致最终出现误清除掉活跃对象等情况,这对于内存管理而言,是十分严重的错误了。 所以为了解决这个问题,在一些编程语言中,常见的做法是,三色标记分为 3 个阶段:

  1. 初始化阶段,需要 STW,包括标记根对象等操作
  2. 主要标记阶段,该阶段支持并行
  3. 结束标记阶段,需要 STW,确认对象标记无误

通过这样的设计,至少可以使得标记耗时较长的阶段可以和用户程序并行执行,大幅度缩短了 STW 的时间,但是由于最后一阶段需要重复扫描对象,所以 STW 的时间还是不够理想,因此引入了内存屏障等技术继续优化。

内存屏障技术

三色标记算法在并发环境下会出现对象丢失的问题,为了解决这个问题,Go 引入了内存屏障技术。

内存屏障技术是一种屏障指令,确保屏障指令前后的操作不会被越过屏障重排。

垃圾回收中的屏障更像是一个钩子函数,在执行指定操作前通过该钩子执行一些前置的操作。

对于三色标记算法,如果要实现在并发情况下的正确标记,则至少要满足以下两种三色不变性中的其中一种:

  • 强三色不变性: 黑色对象不指向白色对象,只会指向灰色或黑色对象
  • 弱三色不变性:黑色对象可以指向白色对象,但是该白色对象必须被灰色对象保护着(被其他的灰色对象直接或间接引用)

插入写屏障

插入写屏障的核心思想是:在对象新增引用关系时,将被引用对象标记为灰色。

go 复制代码
// 插入写屏障伪代码
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    shade(ptr) // 将新引用的对象标记为灰色
    *slot = ptr
}

插入写屏障是一种相对保守的策略,相当于有可能存活的对象都会被标灰,满足了强三色不变行,缺点是会产生浮动垃圾(没有被引用但却没被回收的对象),要到下一轮垃圾回收时才会被回收。

栈上的对象在垃圾回收中也是根对象,但是如果栈上的对象也开启插入写屏障,那么对于写指针的操作会带来较大的性能开销,所以很多时候插入写屏障只针对堆对象启用,这样一来,要保证最终标记无误,在最终标记结束阶段就需要 STW 来重新扫描栈空间的对象进行查漏补缺。实际上这两种方式各有利弊。

删除写屏障

删除写屏障的核心思想是:在对象删除引用关系时,将被解引用的对象标记为灰色。 这种方法可以保证弱三色不变性,缺点是回收精度低,同样也会产生浮动垃圾。

go 复制代码
// 删除写屏障伪代码
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    shade(*slot) // 将被删除引用的对象标记为灰色
    *slot = ptr
}

混合写屏障

Go 1.8 引入了混合写屏障,同时应用了插入写屏障和删除写屏障,结合了二者的优点:

go 复制代码
// 混合写屏障伪代码
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    shade(*slot)  // 删除写屏障
    *slot = ptr
    shade(ptr)    // 插入写屏障
}

GO 中垃圾回收机制

大致演进与版本改进

  • ​Go 1.3之前​:传统标记-清除,全程STW(秒级停顿)。
  • ​Go 1.5​:引入并发三色标记,STW降至毫秒级。
  • ​Go 1.8​:混合写屏障优化,STW缩短至微秒级。
  • ​Go 1.12+​:并行标记优化,提升吞吐量。

在 GO 1.7 之前,主要是使用了插入写屏障来保证强三色不变性,由于垃圾回收的根对象包括全局变量、寄存器、栈对象,如果要对所有的 Goroutine 都开启写屏障,那么对于写指针操作肯定会造成很大的性能损耗,所以 GO 并没有针对栈开启写屏障。而是选择了在标记完成时 STW、重新扫描栈对象(将所有栈对象标灰重新扫描),避免漏标错标的情况,但是这一过程是比较耗时的,要占用 10 ~ 100 ms 时间。

于是,GO 1.8 开始就使用了混合写屏障 + 栈黑化 的方案优化该问题,GC 开始时全部栈对象标记为黑色,以及标记过程中新建的栈、堆对象也标记为黑色,防止新建的对象都错误回收掉,通过这样的机制,栈空间的对象都会为黑色,所以最后也无需重新扫描栈对象,大幅度地缩短了 STW 的时间。当然,与此同时也会有产生浮动垃圾等方面的牺牲,没有完成的方法,只有根据实际需求的权衡取舍。

主要特点

  1. 并发回收:GC 与用户程序同时运行
  2. 非分代式:不按对象年龄分代
  3. 标记清除:使用三色标记算法
  4. 写屏障:使用混合写屏障
  5. STW 时间短:平均在 100us 以内

垃圾回收触发条件

  • 内存分配达到阈值
  • 定期触发
  • 手动触发(runtime.GC())

GC 过程

  1. STW,开启写屏障
  2. 并发标记
  3. STW,清除标记
  4. 并发清除
  5. 结束

总结

Go 语言的垃圾回收机制经过多次优化,已经达到了很好的性能。它采用三色标记算法,配合混合写屏障技术,实现了高效的并发垃圾回收。虽然还有一些不足,如不支持分代回收,但对于大多数应用场景来说已经足够使用。

要优化 Go 程序的 GC 性能,可以:

  1. 减少对象分配
  2. 复用对象
  3. 使用合适的数据结构
  4. 控制内存使用量

参考资料:

相关推荐
风象南2 分钟前
SpringBoot 控制器的动态注册与卸载
java·spring boot·后端
前端付豪24 分钟前
17、自动化才是正义:用 Python 接管你的日常琐事
后端·python
我是一只代码狗28 分钟前
springboot中使用线程池
java·spring boot·后端
PanZonghui44 分钟前
Centos项目部署之安装数据库MySQL8
linux·后端·mysql
Victor3561 小时前
MySQL(119)如何加密存储敏感数据?
后端
用户3966144687191 小时前
TypeScript 系统入门到项目实战-慕课网
后端
guojl1 小时前
Dubbo SPI原理与设计精要
后端
Lemon程序馆1 小时前
搞懂 GO 的垃圾回收机制
后端·go