没想到,Go语言垃圾回收是这样工作的!

简介: 没想到,Go语言垃圾回收是这样工作的!

1. 垃圾回收概述

1.1 什么是垃圾回收

垃圾回收 (Garbage Collection,GC) 是一种自动内存管理的机制,用于自动释放那些不再被程序使用的内存。

它的主要思想是程序在申请内存时不需要释放,而是由垃圾回收器在程序运行的过程中找出那些不再使用的内存并回收它们。

这与 C/C++语言中的手动内存管理形成对比,C/C++程序员需要自己跟踪内存的分配和释放。

而 Go 语言中内存的分配是自动的,程序员主要针对业务逻辑而不需要关心内存的控制,这简化了程序的开发。

1.2 Go 语言中的垃圾回收机制

Go 语言内置了高效的垃圾回收器,它采用了并行标记清除算法。垃圾回收器会监控程序运行时的内存分配情况,找出那些不再使用的变量所占用的内存区域,然后释放掉这些内存以供后续的分配使用。

Go 中的垃圾回收主要有以下几个特征:

(1)并发垃圾回收:垃圾回收器会启动一个单独的 goroutine 执行回收工作,这样就不会阻塞主程序的运行。

(2)准确性:只有不再使用的对象才会被判定为垃圾回收。

(3)高效:垃圾回收的总开销很低,通常 around5%-10%。

1.3 为什么选择垃圾回收

相比于手动内存管理,垃圾回收器的优势主要体现在:

(1)简化内存管理,降低程序员工作量。程序员不再需要关心内存的分配、释放等细节。

(2)提高程序健壮性,避免手动内存管理产生的问题,如内存泄漏、野指针等。

(3)提高开发效率。程序员可以更多地关注业务逻辑。

(4)内存管理的工作由专门的垃圾回收器执行,效率更高。


 

2. 垃圾回收基本原理

2.1 基于标记-清除算法

Go 语言的垃圾回收器基于“标记-清除”(Mark and Sweep)算法以及分代回收策略。

标记-清除算法分为两个阶段:

(1)标记阶段:垃圾回收器会先扫描所有的根对象,标记正在使用的对象,然后遍历这些对象继续标记它们引用的对象,重复这个过程直到所有 reachable 对象都被标记完成。

(2)清除阶段:垃圾回收器会遍历整个内存空间,把那些没有被标记的对象清理掉。这些未被标记的对象就是不可达对象,已经死亡,占用的内存可以回收。


import "fmt"
//对象obj1和obj2互相引用,无法被回收var obj1 = &Object{a: 1} var obj2 = &Object{a: 2}obj1.ref = obj2obj2.ref = obj1
//obj3无法被访问,可以被回收var obj3 = &Object{a: 3}
// 垃圾回收时会标记obj1和obj2// 而obj3不可达所以不会标记

上面代码中,obj1 和 obj2 互相引用,因此它们都可以被标记,不会被回收。而 obj3 没有任何引用,无法可达,所以会被垃圾回收器回收。

2.2 三色抽象模型

为了描述未被标记的对象是否与标记过的对象相关联,抽象出三种颜色用于区分不同对象的标记过程,这就是三色抽象模型。

白色(white):这个对象没有被标记,并且没有被任何标记过的对象引用。白色对象一定是不可达对象。

灰色(gray):灰色对象自身被标记了,但它引用的子对象还没有被标记。灰色对象可能是可达的,也可能是不可达的。

黑色(black):黑色对象自身和它引用的所有子对象都已被标记完成。黑色对象一定都是可达的。

aed348477b8f065c475957959d3b2122_640_wx_fmt=png&from=appmsg&wxfrom=5&wx_lazy=1&wx_co=1.png

三色抽象模型将对象按照其标记过程区分为白、灰、黑三种颜色。其中,只有黑色对象是可达的,白色对象是不可达的可以直接回收,灰色对象只有在它引用的所有子对象都被标记完成(变黑)之后,它才能确定是否是可达的。


 

3. 垃圾回收实现

3.1 双色标记法

Go 语言的实现使用了双色标记法,对象标记为黑或白两种颜色。双色标记法只使用黑白两种颜色区分对象,可以减少一次遍历,提高回收效率。

算法流程:

(1)先将根对象标记为灰色,插入灰色对象工作列表

(2)取出工作列表的下一个灰色对象 obj

(3)扫描 obj 的子对象,将引用的白色子对象标记为灰色,插入灰色工作列表

(4)将 obj 标记为黑色

(5)重复步骤 2 至 4,直到灰色工作列表为空

最后白色的对象就不可达,可以被回收。


func GC() {    grayObjects = makeQueue()      grayObjects.append(rootObject)        for !grayObjects.empty() {        obj = grayObjects.pop()        for child in obj.children {            if child.color == white {                child.color = gray        grayObjects.append(child)             }        }          obj.color = black    }        // 白色对象可以被回收    sweepWhiteObjects() }

与三色标记法相比,双色标记法减少了一次黑色对象扫描,因此效率更高。

3.2 三色标记法

三色标记法会使用白灰黑三种颜色对对象进行标记。

算法流程:

(1)先所有对象标记为白色,将根对象标记为灰色,插入灰色工作列表。

(2)取出灰色工作列表中的对象,扫描它的子对象。如果子对象为白色,则标记为灰色,插入灰色工作列表。

(3)灰色对象处理完后,标记为黑色。

(4)重复步骤 2、3,直到灰色工作列表为空。

(5)最后对所有的白色对象进行回收。


func GC() {    for all objects {        markWhite(object)      }      markGrey(root)    greySet = {root}    while !greySet.empty() {        node = getOneNode(greySet)         markBlack(node)          for child in node.children {            if child.color == white {                child.color = grey                greySet.put(node)              }        }    }        sweep(white) //回收白色对象}

相比双色标记法,三色标记法需要第二次遍历黑色对象来确定其引用的白色对象是否应该回收,效率较低。但三色标记法理论上描述更简捷准确。

3.3 标记-清除 vs 标记-整理

除了标记-清除算法,还有标记-整理(Mark and Compact)算法。两者的主要差别在于清除/整理阶段:

清除:直接回收被标记的对象,留下不连续的闲置内存块。

整理:让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

标记-清除会产生大量不连续的内存碎片,而标记-整理算法没有碎片问题,且内存利用率高。但整理 phase 需要移动大量对象,复制开销很大,效率低于清除。

Go 语言中前期是使用标记-清除算法来实现的内存回收,后期不能满足并发的实时性需求,换成了三色标记清除算法。


 

4. 垃圾回收优化

4.1 缩短 STW 时间

STW(Stop The World) 指程序执行暂停,阻塞主程序进程直到垃圾回收完成。这会间接导致响应延迟、界面卡顿等问题。

Go 语言做了一些优化来缩短 STW 时间:

(1)增量标记:将一个完整周期的标记过程分割为几个子阶段,避免长时间停顿。

(2)并发回收:使用一个单独的回收器 goroutine 执行回收,与用户 goroutine 并发。

(3)写屏障技术:通过引入屏障控制指令顺序执行,从而消除栈扫描。

4.2 边界标记

边界标记可以减少不必要的对象扫描,优化标记过程。当垃圾回收开始时,会从栈底标记相关对象,这层标记就成了边界,超过这层的对象都不需要扫描判断。


func main() {    a := 1     if true {        b := 2        // 垃圾回收执行时,只需要标记stack底部,超过的不扫描    }}

如上代码, b 变量在 if 作用域里,当垃圾回收时只要标记 main 函数栈底下的 a 对象即可,不存在 b 变量引用 escaping 的情况, b 所占用的内存一定可以被回收,无需扫描标记判断。

4.3 最佳配对分配器

最佳配对分配器 (Best-fit/first-fit) 指的是维护一个大小表,记录各个容量的内存块信息,在分配时根据所需内存大小找到最匹配的内存块切分分配,减少内存碎片。

最佳配对方式比一般的顺序分配内存方式会更高效。Go 语言中也应用了最佳配对分配器的思想。

4.4 写屏障优化

标记阶段其中一个开销就是扫描栈区,需要停止 goroutine 来阻塞栈变化。

写屏障 (Write Barrier) 可以不需要扫描整个栈就能保证标记的准确性。其原理类似于内存屏障,通过在指针赋值操作插入写屏障指令控制代码执行顺序,从而省去栈扫描的开销。


 

5 Go 语言垃圾回收器

5.1 GC 启动原理

Go 语言中垃圾回收器是由两个指标控制的:

(1)最近 N 秒的内存分配消耗过快,增长突破某个门限。

(2)距离上一次 GC 超过最长门限 T。

当系统内存分配增长速度过快或者距离上次 GC 过久时,就会触发下一次 GC 执行。


// 假设最近5秒内存分配增长量 > 50MB 会触发GC// 或者距离上一次GC > 2min 也会触发GC
func AllocMemory() {    // Allocated > 50 MB     // LastGC > 2 min    runtime.GC() }

5.2 GC 触发条件

详细的 GC 触发逻辑,可以通过 GOGC、GOMAXPROCS 等环境变量调整:

GOGC 值控制内存分配增长量速度(默认 100,表示允许增长到上限的 100%)。

GOMAXPROCS 影响最大内存增长量。

GCTIMEOUT 控制触发 GC 的最长时间间隔。

5.3 GC 常见配置

通过调整相关环境变量可以配置 GC 策略:

GOGC: 内存增长率(默认 100)。

GOMAXPROCS: 可并行 GC 使用的 CPU 逻辑核心数(默认机器总 CPU 核心数)。

GCTIMEOUT: 触发 GC 的最长时间间隔(默认 2 分钟)。

例如,想要降低 GC 频率,提高应用程序吞吐,可以配置:


GOGC=300 GOMAXPROCS=1 GCTIMEOUT=5m

这样可以减少 GC 次数,但整体内存使用会增加。需要根据实际情况 tuning 找到最佳配置。


 

6. 垃圾回收工作流程

6.1 垃圾回收准备

当垃圾回收被触发时,先会执行 STW,停止所有的 goroutine,然后做一些准备工作:

回收运行时内存分配池中未使用的内存块

返回所有空闲 mspan 结构体

检查所有闲置 mcache 的本地缓存列表

完成这些后就可以开始 GC 标记过程。

6.2 快照标记

标记阶段先会扫描所有 goroutine 的栈, mark survivals , 遍历所有指针域找到 reachable 对象。

同时开启新的 GC goroutine 来帮助标记 assist work, 减少 STW 耗时。

6.3 标记完成

标记过程都是增量进行,后台 GC goroutine 完成标记后,主 goroutine 的标记也基本完成。这时会有一个少量任务清单由 GC worker picks up,最后完成整个标记过程。

6.4 清扫复制

标记完成后就进入清扫阶段,主 goroutine 负责清扫工作,同时可能也会开启一些额外的 GC goroutine 帮助处理残留对象复制等事宜。

6.5 记录清扫时间

最后,主 goroutine 负责统计这次 GC 过程的耗时,更新相关内存统计计数器,为下次 GC 做准备。至此,整个 GC 过程全部结束。


 

总结

Go 语言的高效垃圾回收器采用了标记-清除算法以及染色法,通过写屏障、并发回收等技术实现了低延迟的内存回收。

得益于生态成熟的 GC 策略, Go 语言可以很好地支撑大规模服务,而开发团队也可以更专注于业务开发而不必操心内存控制。未来 Go GC 会引入 分代回收以及压缩 等机制继续进行优化升级。

目录
相关文章
|
19天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
31 7
|
18天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
19天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
93 71
|
18天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
100 67
|
19天前
|
存储 Go
go语言中映射
go语言中映射
32 11
|
21天前
|
Go
go语言for遍历映射(map)
go语言for遍历映射(map)
31 12
|
20天前
|
Go 索引
go语言使用索引遍历
go语言使用索引遍历
28 9
|
20天前
|
Go 索引
go语言使用range关键字
go语言使用range关键字
25 7
|
20天前
|
Go 索引
go语言修改元素
go语言修改元素
26 6
|
10天前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数