Go --- Go语言垃圾处理

概念

  • 垃圾回收(GC-Garbage Collection)
  • 暂停程序业务逻辑SWT(stop the world)
  • 程序根节点:程序中被直接或间接引用 的对象集合,能通过他们找出所有可以被访问到的对象,所以Go程序的根节点通常包括以下几个对象
    • 程序的全局变量和静态变量
    • 程序的调用栈中的变量
    • 当前执行的goroutine

知识点

创建的值在物理内存中,但是物理内存终归是有限的所以需要有垃圾回收(GC)机制。Go语言标准工具链为每一个应用程序都提供了一个runtime库,这个runtime库包含一个垃圾收集器。

为什么要有垃圾回收机制。

go里面的值存在了哪里?

存储在局部变量里的非指针的Go值通常不交由Go GC 管理,Go 将安排分配与创建它的词法范围相关的内存,这种内存分配方式往往比Go GC管理来的更有效,因为Go编译器可以知道预先知道何时释放内存和清除内存。这种分配方式被称为"栈分配"(stack allocation),因为变量存放在goroutine栈空间上。

Go编译器无法确定其生命周期,无法以这种方式分配内存的Go值被称为逃逸到堆(escape to the heap),可以将堆看成是一个内存分配的大包裹,当Go值需要分配内存时,就往里边装。在堆上进行内存分配的行为为动态内存分配,这种类型的内存分配不会假设Go值合适使用和何时释放。如此需要清理这些内存就需要用到Go GC------专门识别和清理动态内存分配的系统了。

虽然GC并不管理栈上的内存,但是扔然需要扫描调用栈中的变量,以确保不会回收被其他对象引用的变量

为什么Go值需要逃逸到堆,一个可能的原因是它的大小是动态决定的,如切片(slice)。需要注意的是,这个逃逸是传递的,如果一个Go值由一个已经逃逸到堆的Go值引用,那这个也肯定是逃逸的。

Go值是否逃逸需要依据上下文和Go编译器的逃逸算法,这个算法非常复杂,而且会随着版本更迭。如果想要更多的了解可以去看 eliminating heap allocations

追踪垃圾回收

垃圾回收也许指的是不同自动回收内存的方法,如引用计数。但本文档介绍的是垃圾回收指的是跟踪垃圾回收,他通过跟随指针来识别正在使用的、活动的对象。

首先让我们先明确一下定义:

对象------对象是一块动态分配的内存,包含一个或多个 Go 值

指针------引用对象内任何值的内存地址。这自然包括 *T 形式的 Go 值,但也包括部分内置 Go 值。如字符串、切片、通道、映射和接口值都包含 GC 必须跟踪的内存地址。

对象和指向其他对象的指针一起形成对象图 ,为了识别实时内存,GC 从程序的根 部开始遍历对象图,这些指针标识 程序肯定正在使用的对象 。根的两个例子是局部变量和全局变量。遍历对象图的过程称为扫描

这个基本的算法是所有追踪GC所共有的,不同追踪GC的不同之处在于一旦发现内存处于活跃状态,他们会采取什么措施。就Go GC而言,他采用的是标记-清除技术 ,这意味着为了跟踪其进度,GC会将其遇到得值标记 为活跃状态。等待跟踪完毕后,GC会遍历堆中的所有内存,并使所有未标记的为可供分配的内存。这个过程称为清除

你可能熟悉的一种替代技术是将对象实际移动到内存的新部分并留下转发指针,该指针稍后用于更新所有应用程序的指针。我们将这种移动对象的GC称为移动GC; Go 有一个不动的 GC。

GC 周期

由于Go GC是标记-清除GC,因此它大致可以分为两部分运行:标记阶段和清除阶段,在进行标记时可能仍有未标记的对象被引用而处在活跃状态,所以清除行为必须是与标记行为完全分开的。此外,当没有GC相关工作要做时,GC也可能根本不活动。GC在所谓的GC周期中不断的轮流执行清除、关闭和标记这三个阶段。处于本文档的目的,请考虑从清除、关闭、然后标记开始的GC循环。

接下来的几节将重点关注建立对 GC 成本的直觉,以帮助用户根据自己的利益调整 GC 参数。

// 后面的区域以后再来探索吧。原文地址,需要小猫。

https://go.dev/doc/gc-guide

算法详情

不同版本的GC算法不同:

  • V1.3 标记-清除算法
  • V1.5 三色并发标记法
  • V1.8 混合屏障机制

标记清除算法

上面已经介绍了。

具体步骤

第一步:开始SWT,暂停程序业务逻辑, 分类出可达和不可达的对象,然后做上标记

第二步:开始标记,程序找出它所有可达的对象,并做上标记。

第三步: 标记完了之后,然后开始清除未标记的对象。

第四步:停止SWT,让程序继续跑。

缺点(不足):

标记算法清晰明了,但是有非常严重的问题。

  • 首先是STW,会让程序暂停,让运行出现卡顿
  • 在标记阶段会扫描整个堆。
  • 清除数据会产生堆碎片

虽然做了简单的优化(将清除阶段(第三步)和停止SWT(第四步)交换位置),但是优化后的算法仍然存在最大的问题就是使用了STW,算法会暂停整个程序。

三色并发标记法

所谓的三色标记法就是使用三种不同的状态来标记对象,根据对象状态的不同来确定需要清除的对象有那些。

具体步骤

第一步:新创建的对象,默认的颜色都是标记为"白色"。

第二步:开始从程序根节点开始遍历对象,找到可达对象,将可达对象从白染灰。注意:不是一下子将所有白色节点都遍历了,只是找到了根节点可达的白色节点。

第三步:遍历灰色节点找到可达对象,将可达对象染灰,然后该灰色节点染黑。

第四步:重复第三步,直到所有灰色节点均无可达对象,然后将所有灰色节点染黑。

第五步:回收所有白色节点(这些都是程序不可达的对象,是需要被清理的对象),省下的就只剩程序可达的黑色节点。

根据上述步骤,我们并没有发现他解决了需要引入STW而导致程序卡顿的问题。因为在遍历灰色节点寻找可达对象时为了保证数据安全还是会选择开始三色标记之前就加上STW。

为什么非要使用STW不可呢?

接下来我们分析一下以下情况。

一、当GC在三色标记的标记阶段,程序运行中出现黑色节点的对象引用了白色节点的对象,当然如果该白色节点恰好是可达对象,只是还没被GC扫描到哪还好说,但是如果该白色节点恰好是程序不可达对象,就会出现引用的对象在扫描结束后被清除的情况。一个被引用的对象就这样被清除了。

二、在标记过程中,一个灰色节点指向一个白色节点的引用指针被运行中的程序移除,同时一个黑色节点多了一个指向该白色节点的引用指针,等标记过后,发现该白色指针并没有被标记为黑色。一个被引用的对象就这样被清除了。

为了避免这两种情况出现,最简单的方法就是在标记之前启动STW,清除过后停止STW,但是会较大程度上影响程序的运行。在上面两种情况对应了两个条件。

一、不希望黑色节点后面直接连接白色节点

二、不希望灰色节点在黑色节点连接白色节点后被清除了与该白色节点的可达关系

因为我们并不希望使用STW,那么有什么方法能破坏这两个条件呢?

屏障机制

了解屏障机制之前需要先了解两个概念:

一、强三色不变式:不存在黑色节点引用到白色节点的指针。强制性不允许黑色节点指向白色节点。

二、弱三色不变式:所有被黑色节点引用的白色节点都处于灰色保护状态即被引用的白色节点终会在标记阶段变为可达对象。

接下来在来看两种屏障机制:

一、插入写屏障:

​ 具体操作:在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)

​ 满足强三色不变式:因为引入的对象都是黑色或灰色(白色节点变为灰色节点)。

黑色节点所在的内存空间有两个地方,一个是栈,一个是堆。栈的空间较小,但是要速度快,要负责函数的频繁调用,因此不在栈空间上使用插入屏障(不保障满足强三色不变式,黑色节点可以指向白色节点)。插入屏障只在堆空间上使用。

尽然这样就出现一个问题,在进行一次标记后,不能确保白色节点没有被引用。所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束。

虽然还是使用了STW,不过相比于标记整个程序的内存空间,只标记栈上的空间有了很明显的效率提升。

二、删除写屏障

具体操作:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。

满足弱三色不变式:保护灰色对象到白色对象的路径不会断。

这样也会有问题,比如一次GC周期不会删除GC启动时的可达但是过程中变为不可达的对象。

虽然引入了屏障机制,我们发现还是有不足之处:

  1. 插入写屏障:结束时需要STW来重新扫描栈。
  2. 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

接下来就看看GoV1.8是如何处理的

混合写屏障(hybrid write barrier)机制

规则:

1、GC扫描栈空间上的可达对象,并全部染为黑色。(之后不再进行第二次重复扫描,无需STW)。

2、GC期间,任何在栈上创建的新对象,均为黑色。

3、GC期间,被删除的对象标记为灰色。

4、GC期间,被添加的对象标记为灰色。

满足变相的弱三色不定式。

注意:混合写屏障也不在栈空间上使用,因为栈要求的是运行速率。

只有在GC执行时才会有屏障规则,普通条件下并不使用。

Golang中的混合写屏障满足弱三色不变式,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行重新扫描操作了,减少了STW的时间。

对比:

GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。

GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通

GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

参考文章

语雀-Golang修养之路-刘丹冰Aceld
A Guide to the Go Garbage Collector
知乎-GC 机制中,所谓的"根节点"具体指的是什么?-orionnnn

相关推荐
Java水解16 分钟前
微服务架构下Spring Session与Redis分布式会话实战全解析
后端·spring
Moe48816 分钟前
如何使用 Spring Cache 结合 Redis 和 Caffeine 构建二级缓存机制
后端
CoovallyAIHub1 小时前
OpenClaw 近 2000 个 Skills,为什么没有一个好用的视觉检测工具?
深度学习·算法·计算机视觉
CoovallyAIHub1 小时前
CVPR 2026 | 用一句话告诉 AI 分割什么——MedCLIPSeg 让医学图像分割不再需要海量标注
深度学习·算法·计算机视觉
Json_Lee1 小时前
2026 年了,多 Agent 编码该怎么选?agent-team vs Claude Agent Teams vs Claude Squad vs Met
前端·后端·vibecoding
CoovallyAIHub1 小时前
Claude Code 突然变成了 66 个专家?这个 5.8k Star 的开源项目,让我重新理解了什么叫"会用 AI"
深度学习·算法·计算机视觉
陈随易1 小时前
刚上市就断货?如此火爆的编程显示器到底有什么魔力
前端·后端·程序员
兆子龙1 小时前
前端哨兵模式(Sentinel Pattern):优雅实现无限滚动加载
前端·javascript·算法
ray_liang2 小时前
一小时手搓轻量级可代替 Qdrant 的向量数据库
后端·架构
昵称为空C2 小时前
spring-ai mcp-server(ssh工具)
后端·ai编程