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

目录
相关文章
|
5天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
3天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
11 2
|
3天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
14 2
|
7天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
84 58
|
6天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
3天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
10 4
|
3天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
14 1
|
7天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
7天前
|
Go 数据处理 调度
探索Go语言的并发模型:Goroutines与Channels的协同工作
在现代编程语言中,Go语言以其独特的并发模型脱颖而出。本文将深入探讨Go语言中的Goroutines和Channels,这两种机制如何协同工作以实现高效的并发处理。我们将通过实际代码示例,展示如何在Go程序中创建和管理Goroutines,以及如何使用Channels进行Goroutines之间的通信。此外,本文还将讨论在使用这些并发工具时可能遇到的常见问题及其解决方案,旨在为Go语言开发者提供一个全面的并发编程指南。
|
5天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。