当前位置: 首页 > backend >正文

golang 垃圾收集机制

背景

垃圾回收(Garbage Collection,GC) 是Go语言的核心特性之一,是实现内存自动管理的一种形式。golang的自动垃圾回收屏蔽了复杂且容易出错的内存操作,让开发变得更加简单、高效。在Go语言中,从实现机制上来说,垃圾回收可能是最复杂的模块了。了解垃圾回收的机制,有助于更好地理解Go语言的内存管理机制,从而更好的使用Go语言进行开发。

定义

  • **STW(Stop The World):**指的是在垃圾回收期间,Go 会暂停所有正在运行的程序 Goroutine,以便安全地执行内存清理操作。STW 会让所有业务逻辑短暂停止运行,导致请求处理变慢甚至出现延迟抖动,特别是在高并发或对响应时间要求严格的场景中会影响用户体验。
  • **写屏障(Write Barrier):**这个概念乍一听有点硬核,但其实可以类比成一个"门卫"或者"报警器",它的作用就是:在内存被修改(写入)时,拦一下,顺便记录或执行一些操作,以保证垃圾回收的正确性。
  • 根对象:
    • 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
    • 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
    • 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。

垃圾收集

常见的 GC 算法

引用计数法

根据对象自身的引用计数来回收,当引用计数归零时进行回收,但是计数频繁更新会带来更多开销,且无法解决循环引用的问题。

  • 优点:简单直接,回收速度快
  • 缺点:需要额外的空间存放计数,无法处理循环引用的情况;

标记清除法

标记出所有不需要回收的对象,在标记完成后统一回收掉所有未被标记的对象。
在这里插入图片描述

  • 优点:简单直接,速度快,适合可回收对象不多的场景
  • 缺点:会造成不连续的内存空间(内存碎片),导致有大的对象创建的时候,明明内存中总内存是够的,但是空间不是连续的造成对象无法分配;

复制法

复制法将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉

在这里插入图片描述

  • 优点:解决了内存碎片的问题,每次清除针对的都是整块内存,但是因为移动对象需要耗费时间,效率低于标记清除法;
  • 缺点:有部分内存总是利用不到,资源浪费,移动存活对象比较耗时,并且如果存活对象较多的时候,需要担保机制确保复制区有足够的空间可完成复制;

标记整理法

标记过程同标记清除法,结束后将存活对象压缩至一端,然后清除边界外的内容

在这里插入图片描述

  • 优点:解决了内存碎片的问题,也不像标记复制法那样需要担保机制,存活对象较多的场景也使适用;
  • 缺点:性能低,因为在移动对象的时候不仅需要移动对象还要维护对象的引用地址,可能需要对内存经过几次扫描才能完成;

分代法

将对象根据存活时间的长短进行分类,存活时间小于某个值的为年轻代,存活时间大于某个值的为老年代,永远不会参与回收的对象为永久代。并根据分代假设(如果一个对象存活时间不长则倾向于被回收,如果一个对象已经存活很长时间则倾向于存活更长时间)对对象进行回收。

比较经典的就是 Java 中的分代 GC 了

Go 语言垃圾收集

Go 语言 GC 主要使用的是三色标记法的 GC 算法,会在下文中介绍到,Go 语言中 GC 时的 STW 的情况还是存在的,只是说在不断的迭代去尽量减少 STW 对程序运行产生的影响。

三色标记法

传统的标记清除算法中,垃圾收集器从垃圾收集的根对象出发,递归遍历这些对象指向的子对象并将所有可达的对象标记成存活;标记阶段结束后,垃圾收集器会依次遍历堆中的对象并清除其中的垃圾,整个过程需要标记对象的存活状态,用户程序在垃圾收集的过程中也不能执行,我们需要用到更复杂的机制来解决 STW 的问题,这就出现了三色标记法。

三色标记算法将程序中的对象分成白色、黑色和灰色三类:

  • **白色对象(可能死亡):**未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。
  • **灰色对象(波面):**已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。
  • **黑色对象(确定存活):**已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。
标记过程
  1. 起初所有的对象都是白色的;
  2. 从根对象出发扫描所有可达对象,标记为灰色,放入待处理队列;
  3. 从待处理队列中取出灰色对象,将其引用的对象标记为灰色并放入待处理队列中,自身标记为黑色;
  4. 重复步骤(3),直到待处理队列为空,此时白色对象即为不可达的“垃圾”,回收白色对象;

在这里插入图片描述

并发问题

在这里插入图片描述

指的是在用户协程与 GC 协程并发执行的场景下,部分存活对象未被正确标记,从而被误删的情况。如图所示:

  • 初始时刻: C 持有 D 的引用;
  • GC 协程:A 被 GC 扫描完成,不会再次扫描,标记为黑色(此时 C 为灰色,还未完成扫描,D 为白色);
  • 用户协程:A 建立对 D 的引用,C 删除对 D 的引用;
  • GC 协程:开始扫描 C,C 被置为黑色,由于 C 删除了对 D 的引用,此时 D 未被扫描到,依然为白色,但其被 A 对象引用,不是该被回收的对象;
  • GC 协程:标记完成,回收白色对象,D 被误删除。

漏标的情况不能容忍,因为删除了程序正在使用的对象,会造成程序异常。

为了解决上述并发的问题,后续就引入了写屏障一内容

写屏障

强弱三色不变式

  • 强三色不变式

    • 不存在黑色对象引用到白色对象的指针。
    • 在这里插入图片描述
  • 弱三色不变式

    • 所有被黑色对象引用的白色对象都处于灰色保护状态.
    • 在这里插入图片描述

Dijkstra 插入写屏障

在这里插入图片描述

  • 插入写屏障:规则维护了强三色不变式,保证当一个黑色对象指向一个白色对象前,会先触发屏障,将白色对象置为灰色,再建立引用。

Yuasa 删除写屏障

在这里插入图片描述

  • 删除写屏障:实现了弱三色不变式,保证当一个白色对象即将被上游删除引用前,会触发屏障将其置灰,之后再删除上游指向其的引用。但是引入删除写屏障,有一个弊端,就是一个白色对象的引用被删除后,置灰,即使没有其他存活的对象引用它,它仍然会活到下一轮。如此一来,会产生很多的冗余扫描成本,且降低了回收精度。

混合写屏障机制

在 Go 1.8 版本之前,栈上的对象在并发标记阶段可能会发生变化,导致需要在标记结束后进行一次 STW 操作并重新扫描栈上的对象,以确保所有存活的对象都被正确标记。然而,这个过程会增加垃圾收集的暂停时间,影响程序的性能。

在 Go 1.8 版本后,引入了混合写屏障机制(hybrid write barrier),避免了对栈 re-scan 的过程,极大的减少了 STW 的时间。混合写屏障的精度虽然比插入写屏障稍低,但它完全消除了对栈的 STW 重新扫描,从而进一步减少了 STW 的时间。

Go 1.8 版本后的 GC 混合写屏障有以下四个关键步骤:

  1. GC 刚开始的时候,会将栈上的可达对象全部标记为黑色。(在扫描栈帧的过程中,栈上的局部变量会被标记为黑色,因为它们被视为已处理的根对象,不需要再重新扫描)
  2. GC 期间,任何在栈上新创建的对象,均为黑色(避免在扫描过程中,重复扫描这些对象)。
  3. 堆上被删除的对象标记为灰色(删除写屏障)。
  4. 堆上新添加的对象标记为灰色(插入写屏障)。

在标记阶段一开始,所有栈上的对象都会被直接标记为黑色,后续任何在栈上新创建的对象,均为黑色。黑色对象表示已经访问且不需要再扫描其引用,由于栈上的对象已经标记为黑色,不再需要在标记结束后重新扫描栈上的对象,从而减少了 STW 的时间。

一次完整的 GC 流程

初始 STW 事件:

  • 目的:暂停所有 Goroutines 后,内存状态到达一致性,是开启混合写屏障机制的前提,所以暂停所有 Goroutines 是必要的。

并发标记阶段:

  • 开启混合写屏障机制:在初始 STW 事件阶段暂停所有 Goroutines 的同时,垃圾回收器会开启混合写屏障机制。(注意:混合写屏障机制主要是针对堆内存中的对象引用修改而设计的,它的主要目的是确保在垃圾回收过程中,任何新的对象引用修改都能被正确记录和处理)
  • 为什么现在开始混合写屏障机制:暂停所有 Goroutines 后,内存状态到达一致性,一旦 Goroutines 恢复执行,内存状态一致性会被打破,无法得以保证;所以暂停所有 Goroutines 的主要目的是为了顺利开启混合写屏障机制,从而保障后续的内存状态一致性,这是确保垃圾回收过程有效和准确的关键步骤。
  • 混合写屏障机制的作用:记录对象引用的修改,混合写屏障机制在开启后,任何新的对象引用修改都会被记录下来。
    • 确保了在分批扫描 Goroutines 栈帧时,即使其他 Goroutines 被恢复运行并修改对象引用,这些修改也能被正确记录和处理。确保了在并发标记阶段,所有引用的修改会被正确记录和处理。
    • 当对象引用被修改时,混合写屏障会记录这些修改,将新引用的对象标记为灰色(堆上被删除的对象标记为灰色,堆上新添加的对象标记为灰色)。

并发清除阶段:

一次完整的 GC 分为三个阶段:初始 STW 事件、并发标记阶段、并发清除阶段;随着 Go 版本的不断更迭,GC 的各项细节问题也得到了优化,优化方向包括:

  • 增量式改进:通过增量标记和清理,进一步减少了每次 STW 暂停的时间。
  • 优化内存管理:通过基于页的内存管理和减少内存碎片,提升了内存分配和释放的效率。
  • 高效的数据结构:使用更高效的数据结构来维护垃圾回收状态,减少了开销。

总结

现代 Go 版本的垃圾收集器通过一系列优化措施,已经将 STW 时间减少到亚毫秒级别,极大地提高了程序的性能和响应性。未来版本可能会进一步优化垃圾收集器,以实现更短的 STW 时间。

经过上述介绍可能还并不全面,如果有想了解更细的同学可以自己去查看相关的资料。

引用

Golang垃圾回收(GC)介绍

Go 语言之垃圾回收机制

Golang 垃圾回收:一次 GC 周期的详细过程

[Go三关-典藏版]Golang垃圾回收+混合写屏障GC全分析

http://www.xdnf.cn/news/8525.html

相关文章:

  • 防火墙NAT地址组NAT策略安全策略
  • 50 python Matplotlib之Seaborn
  • Python爬虫实战:研究Cola框架相关技术
  • 开发工具整理
  • Python初始Flask框架
  • 敦煌网测评从环境搭建到风控应对,精细化运营打造安全测评体系
  • 【自定义类型-结构体】--结构体类型,结构体变量的创建和初始化,结构体内存对齐,结构体传参,结构体实现位段
  • ComfyUI Chroma解锁文生图新维度;OpenMathReasoning数学推理数据集,首个专注数学推理的高质量数据集
  • 深入探索 CSS 中的伪类:从基础到实战​
  • 文件目录名称无效?数据恢复全流程与常见问题解析
  • CMA/CNAS认证电子签章审计追踪 质检 LIMS 系统应用要点
  • 电子电路:什么是滤波器,什么优势高通滤波器?
  • Cookie、Session、JWT
  • 吃出 “颈” 松:痉挛性斜颈的饮食调养之道
  • Redis从入门到实战 - 原理篇
  • lua脚本实战—— Redis并发原子性陷阱
  • I-CON: A UNIFYING FRAMEWORK FOR REPRESENTATION LEARNING
  • 从Android开发聊技术
  • Python打卡5.23(day24)
  • 【和春笋一起学C++】(十五)字符串作为函数参数
  • 快速开发平台如何选择?技术选型避坑指南与实践洞察
  • el-select中自定义 两组el-option,但是key不一样,并且点击需获取当前整个项的所有属性
  • 前端地图数据格式标准及应用
  • 基于若依的人脸识别(2)——后端实现步骤
  • 开源工具自建AI大模型底座:打造你的专属智能助理
  • GPU训练和call方法
  • 2025电工杯数学建模竞赛A题 问题2 建立基于历史功率的光伏电站日前发电功率预测模型(线性回归,随机森林,SVR模型,集成模型)- 完整代码与结果
  • Linux 进程控制总结
  • 香港维尔利健康科技集团全面推进AI医疗落地,构建智慧健康管理新模式
  • Claude 4 发布:编码 AI 新纪元的开启