没想到,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 会引入 分代回收以及压缩 等机制继续进行优化升级。

目录
相关文章
|
4天前
|
监控 Linux PHP
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
51 20
|
2天前
|
存储 监控 算法
探秘员工泄密行为防线:基于Go语言的布隆过滤器算法解析
在信息爆炸时代,员工泄密行为对企业构成重大威胁。本文聚焦布隆过滤器(Bloom Filter)这一高效数据结构,结合Go语言实现算法,帮助企业识别和预防泄密风险。通过构建正常操作“指纹库”,实时监测员工操作,快速筛查可疑行为。示例代码展示了如何利用布隆过滤器检测异常操作,并提出优化建议,如调整参数、结合日志分析系统等,全方位筑牢企业信息安全防线,守护核心竞争力。
|
10天前
|
Go C语言
Go语言入门:分支结构
本文介绍了Go语言中的条件语句,包括`if...else`、`if...else if`和`switch`结构,并通过多个练习详细解释了它们的用法。`if...else`用于简单的条件判断;`if...else if`处理多条件分支;`switch`则适用于基于不同值的选择逻辑。特别地,文章还介绍了`fallthrough`关键字,用于优化重复代码。通过实例如判断年龄、奇偶数、公交乘车及成绩等级等,帮助读者更好地理解和应用这些结构。
34 14
|
24天前
|
存储 监控 算法
内网监控系统之 Go 语言布隆过滤器算法深度剖析
在数字化时代,内网监控系统对企业和组织的信息安全至关重要。布隆过滤器(Bloom Filter)作为一种高效的数据结构,能够快速判断元素是否存在于集合中,适用于内网监控中的恶意IP和违规域名筛选。本文介绍其原理、优势及Go语言实现,提升系统性能与响应速度,保障信息安全。
28 5
|
1月前
|
算法 安全 Go
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
46 14
|
1月前
|
Go 数据库
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
95 11
|
1月前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
|
9天前
|
监控 关系型数据库 MySQL
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
19 0
|
1月前
|
监控 安全 算法
深度剖析核心科技:Go 语言赋能局域网管理监控软件进阶之旅
在局域网管理监控中,跳表作为一种高效的数据结构,能显著提升流量索引和查询效率。基于Go语言的跳表实现,通过随机化索引层生成、插入和搜索功能,在高并发场景下展现卓越性能。跳表将查询时间复杂度优化至O(log n),助力实时监控异常流量,保障网络安全与稳定。示例代码展示了其在实际应用中的精妙之处。
44 9
|
2月前
|
算法 安全 Go
Go 语言中实现 RSA 加解密、签名验证算法
随着互联网的发展,安全需求日益增长。非对称加密算法RSA成为密码学中的重要代表。本文介绍如何使用Go语言和[forgoer/openssl](https://github.com/forgoer/openssl)库简化RSA加解密操作,包括秘钥生成、加解密及签名验证。该库还支持AES、DES等常用算法,安装简便,代码示例清晰易懂。
64 12

热门文章

最新文章