golang垃圾回收器 GC

本文简述了Go语言的垃圾回收器(GC)的工作原理,包括标记阶段和着色过程。标记阶段通过写屏障算法配合STW(Stop the World)确保内存状态正确,并由多个线程并发进行。着色过程将可达对象标记为黑色,不可达对象标记为白色,以供回收。整个过程保证了GC的高效执行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

提问:请简述 Go 语言的垃圾回收器(Garbage Collector, GC)?

1 | 标记阶段

该阶段对以 span-object 为载体的内存进行扫描。由于 GC 与 Go 语言主程序一起并发执行,所以须要在扫描时监控内存可能出现的状态改变。

写屏障(Write Barrier)算法便应运而生,启动写屏障的唯一途径就是短暂停止 Go 语言主程序,称之为 STW(Stop the World),并以 Start the World 恢复:

在 GC 工作开始时,Go 语言随即为每个处理机 P 配置一个 mark worker线程 G 来帮助标记内存,它们负责为进入 STW 后的清理做前期工作。

一旦根程序(Root)被加入待处理队列 work pool,标记周期便正式开始,这些线程 G 开始遍历 span 并标记内存块 object。

以程序 main.go为例,以下的配图均服务于本实例的三个变量s1s2_

 type struct1 struct {
     a, b int64
     c, d float64
     e    *struct2
 } // 40 bytestype struct2 struct {
     f, g int64
     h, i float64
 } // 32 bytes// go:noinline
 func allocStruct1() *struct1 {
     return &struct1{
         e: allocStruct2(),
     }
 }// go:noinline
 func allocStruct2() *struct2 {
     return &struct2{}
 }func main() {_ = trace.Start(os.Stderr)
     defer trace.Stop()
 ​
     s1 := allocStruct1()
     s2 := allocStruct2()func() {
         _ = allocStruct2()
     }()
 ​
     runtime.GC()
 ​
     fmt.Printf("s1 = %X, s2 = %X\n", &s1, &s2)
 }

因为结构体 struct1struct2 大小分别为 40 和 32 个字节,被分配到 48 和 32 字节大小的 span,后者不包含指针,所以它被存储在非指针的 span 区:

以是否包含指针区分 span,使得 GC 在扫描过程避免不必要的性能开销,这也是 GC 高并发性能彪悍的关键原因。

每当主程序 main 的内存分配完成,GC 的 mark worker 线程便会强制执行一轮的扫描,流程如下:

GC 从栈映射 span 的栈(stack)开始,根据扫描到的指针引导的地址来递归式访问 span,span 一旦被标记为 no span ,后续不会继续被扫描,递归便结束;反之继续根据指针递进。

s1 包含了一个 4 字节大小的指针,指向了 32 字节大小的 span,而 s2 不包含指针,便是递归的尽头。

由于不同的 Goroutine 根据在队列中的指针并发完成,之前入队的background mark worker 会出队,扫描 span 中的 object 然后将找到的指针加入队列:

2 | 着色过程

着色线程 mark worker 利用三色标记算法来完成任务:

  • 起初所有的 object 都被认定为白色
  • 比如栈、堆和全局变量这种 object 被标记为灰色

GC 的这些线程在上述步骤的基础上:

  • 将灰色 object 标记为黑色
  • 将灰色 object 所包含的所有指针所指向的地址都标记为灰色

递归执行以上两个步骤,最终对象非黑即白,其中白色即未被引用且可以被回收的 object。

以上述程序为例,起初所有 object 被视作白色,遍历后将可达者标记为灰色,如果 object 处标记为 no sacan,则视为递归的尽头,被标记成黑色:

灰色的 object 被加入待扫描队列,出队被扫描后即被标记为黑色:

重复执行上述步骤,直至没有待处理的灰色 object:

最终,黑色 object 即为被使用,而白色为可回收。

上述实例中,结构体 struct2 的实例被匿名函数创建被分配至上图的 span class 3,透过汇编源码得知,在栈上不可达:

 TEXT    "".main.func1(SB), ABIInternal, $24-0
 MOVQ    TLS, CX
 PCDATA  $0, $-2
 MOVQ    (CX)(TLS*2), CX
 PCDATA  $0, $-1
 CMPQ    SP, 16(CX)
 PCDATA  $0, $-2
 JLS     64
 PCDATA  $0, $-1
 SUBQ    $24, SP
 MOVQ    BP, 16(SP)
 LEAQ    16(SP), BP
 FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 NOP
 LEAQ    type."".struct2(SB), AX

struct2 的实例 _ 由始至终为白色,必将被 GC 回收,实践上使用匿名变量能有效减少内存开销。

mspan 数据结构中的 gcmarkBits *gcBits 用于实现 span 的颜色标记:

灰色和黑色在 gcmarkBits 中皆为 1,不同之处在于黑色为颜色标记的终点,灰色为待续。

GC 使 Go 程序短暂停止,将对每个写屏障所做的更改更新至 work pool 直至清空队列。

3 | 小结

使用顶配 Intel i-7 四核心处理机和标准库 runtime/trace 可视化分析上述的垃圾回收线程,在命令栏依次执行:

 go run main.go 2> trace.out
 go tool trace trace.out

该命令会多线程执行的序列化结果呈现于浏览器:

GC 为上图顶部淡蓝进度条,随主程序启动后执行;

Proc 0 为标准库 runtime/trace 的监视线程 G19,用作本次资源监控,可以忽略;

Proc 1 为上述 Go 语言测试实例的本身主线程 runtime.main

Proc 1 粉蓝色线程 G2 即为 runtime.gcBgMarkWorker

Proc 0 粉红色前边短暂的绿色为 Stop the World 线程开始处,即 sweep termination

Proc 0/1/4 的粉红色 GC (dedicated) 为 marking worker 线程,就是递归遍历 span 的着色操作;

Proc 4 肉粉色 STW 为线程上述 Stop the World 的短暂停顿结束,最后由箭头交棒给主线程 G1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值