浅析Golang的内存管理(下篇):go垃圾回收机制

文章目录

  • 三色标记算法
  • 混合写屏障
  • 并发、增量回收机制
  • GC触发时机

  go语言作为内存托管类型的开发语言,go runtime提供了自动的内存管理机制,无需程序员手动管理对象的内存释放,go runtime会在合适的时机自动释放不需要的内存对象。

一、三色标记算法

  传统的内存对象标记算法

  早期的开发语言一般使用引用计数,或引用追踪算法来管理,回收那些“不可达”的对象。它们的过程大致如下,对象分配通过一个基地址和偏移量在托管堆上有条不紊的分配,在某次对象分配时,对应运行时发现托管堆的剩余空间不足以分配线程所申请的内存,此时,运行时进行“STW”,停止所有工作线程,垃圾回收线程开始进行工作,从根对象开始扫描,将“不可达”的对象做特殊标记,待扫描结束后,非垃圾对象进行基于托管堆的内存压缩,防止内存碎片的产生。在这个过程中,多次GC扫描的后,一直不被是认为是“不可达”的对象会被提升至更高的“代”,后面再进行垃圾扫描就暂时不扫描它们以提高效率。对应下(图1)和(图2)。

(图1)(图2) 

  三色标记法

  go语言中,垃圾回收算法采用三色标记法,将内存对象抽象为黑、灰、白三类对象。每次创建新对象时,该对象都会被标记为白色。而对象之间的相互引用关系,将整个go进程的内存对象相关关联起来,如同一张网一样(如图3)。

(图3)

  接下来,待到go runtime触发GC时,会依次执行如下步骤:

  • 第一步,GC进程将会从根节点开始遍历所有对象,,将可以遍历到的对象标记为灰色(放入至灰色标记表中),这一次遍历为第一次遍历,目的就是找出接下来循环遍历所需的根节点
    • 在go runtime中,GC过程是由一个工作协程来完成的,这里的GC进程即该工作协程,后续描述都称为GC进程。

    • 在go runtime中,根对象指的是 go routine栈对象 、 全局对象、寄存器中的指针 和 运行时数据结构(如go runtime的调度器、网络轮询器、定时器等)、显式表留的根对象(如sync.Pool中活跃的对象)
  • 第二步,GC进程(这里其实会有用户进程辅助标记)将会遍历第一步找到的所有灰色对象集合,并且将灰色对象引用的对象,从白色标记为灰色,之后将自己标记为黑色。
  • 第三步,重复第二步,直至进程中所有的对象再也没有灰色对象,此时所有可达的对象都遍历完成,所有内存对象只存在两种颜色,即黑色与白色。其中黑色代表进程中,逻辑可达的对象,这部分对象保留;白色的对象则代表不可达的垃圾对象,需要被回收清理。(如图4的对象1、5)
  • 最后一步,清理白色对象,针对白色对象的所占用的内存回收释放。

 (图4) 

二、混合写屏障

  在垃圾回收算法中,有一个很重要的概念叫做STW(Stop The World),即垃圾回收过程中,需要停止其他工作线程的执行,等待GC线程工作完成后,才恢复其他工作线程继续工作。这样,就带来了一个问题,GC会造成程序正常处理任务的时延,假如频繁、长时间的GC,势必会影响整个程序的性能。

  这里延伸下,那为什么GC要有STW? 假如GC过程中,GC线程和工作线程一起执行,将会影响标记的准确性。例如,T1时刻,对象A被根对象引用,GC进程会将对象A标记为灰色。T2时刻,一个工作线程将这个根对象的引用指向另一个对象B,此时对象A应该作为垃圾(因为是不可达的,没有跟对象或其他可达对象对它的引用)而无法被回收。更为严重的情况是,可能会存在有的对象是可达的,而被误认为垃圾对象被回收,造成内存的不安全(见图5)。

(图5)

 

  为了解决GC过程中,内存标记的准确性,内存的安全性,以及在GC过程中不影响其他工作线程的执行(低时延),引入了“写屏障”的概念。

  • 内存屏障技术:https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C

  在了解写屏障之前,需要了解另外一个概念:“不变性原则”。GC为了解决并发标记的正确性,整个标记过程需要遵循两个原则“强三色不变性”与“弱三色不变性”

  • “强三色不变性”:黑色对象不会指向白色对象,只会指向黑色对象或者灰色对象。

  • “弱三色不变性”:黑色对象指向白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。

  强三色不变性

  为了满足“强三色不变性”,go runtime实现了插入写屏障。插入写屏障的行为类似这样,当A对象引用B对象时,B对象被标记为灰色。伪代码如下所示:

添加下游对象(当前下游对象 slot,新下游对象 ptr){标记灰色(新下游对象 ptr)当前下游对象slot = 新下游对象 ptr}

  例如对象A为黑色对象,开始时对象A的下游对象为对象B,现在将对象A的下游对象改为对象C,此时将会触发插入写屏障,直接将对象C标记为灰色(见图6)。(插入写屏障对栈空间不生效,所以再go1.8之前版本的GC要二次扫描栈空间,保证栈空间对象不丢失,这个过程需要STW,一般栈扫描时间为10~100ms

(图6)

  弱三色不变性 

  为了满足“弱三色不变性”,go runtime实现了删除写屏障,当被删除的对象本身为灰色或者白色时,直接标记为灰色。删除写屏障的行为类似这样,A对象引用对象B,B对象引用对象C,此时删除B对C的引用,直接将C标记为灰色。伪代码如下所示:
添加下游对象(当前下游对象 slot,新下游对象 ptr){if (当前下游对象slot是灰色 || 当前下游对象slot是白色{标记灰色(当前下游对象 slot)}当前下游对象slot = 新下游对象 ptr
}

  例如对象A为灰色对象,A的下游对象为对象B,B此时为白色,现在将对象A对B的引用删除(即A的下游对象为空),此时将触发删除写屏障,将B标记为灰色,接下来GC任务扫描时,B对象的下游对象也不会被标为垃圾回收掉(见图7)。(删除写屏障带来的问题是回收精度比较低,很多垃圾对象会延迟到下一轮GC扫描才会被识别、回收内存

(图7) 

  混合写屏障

  go V1.8引入混合写屏障,结合插入写屏障删除写屏障,解决栈对象重新扫描的问题,大大缩短了GC的STW时间。它的工作原理如下所示:

  • GC开始时,将所有栈对象,全都标记为黑色,避免二次扫描栈(进而避免扫描栈空间时STW)。

    • goV1.8之前,go routine栈空间上对象并非都被标记成黑色,只会标记go routine栈上的活跃指针。判断活跃指针的方式为,根据指针映射表(属于go编译器为每个函数生成的栈帧元数据,标记了栈帧中哪些位置存储了指针)中的指针标记,判断栈帧的每个内存槽,匹配的内存槽对象被标识为根对象。

  • GC期间,在栈上创建的对象均被标记为黑色。

  • GC期间,被删除的对象被标记为灰色,被添加的对象标记为灰色。 

添加下游对象(当前下游对象 slot,新下游对象 ptr){标记灰色(新下游对象 slot)标记灰色(新下游对象 ptr)当前下游对象slot = 新下游对象 ptr
}

  例如对象A,B,C为栈区对象,E,F,G为堆区对象(见图8)。 做一些,栈引用堆场景下的,删除、增加引用,堆引用栈场景下的删除、增加引用。

(图8)

三、并发、增量回收机制

  go runtime将原本较长的STW时间的GC任务,分割为多个小的GC时间片,并在应用程序运行期间交替执行,并以写屏障的机制保证内存安全性,最终达到避免GC长时间的暂停。

  goV1.6版本,将垃圾回收器工作的各个阶段抽象为一个状态机,状态机有_GCMark_GCmarktermination_GCoff这3个状态,划分为标记准备阶段(GC Mark Start)、增量标记阶段(GC Mark)、再扫描阶段(Mark Termination)、清除阶段(Sweep)、空闲阶段(Idle)五个运行阶段。通过这个状态机的状态跃迁,任何一个工作协程都能执行GC任务,从而移除中心化GC的限制,无需专门的goroutine在处理器之间同步、协调垃圾回收状态,从而降低了延迟。

  go的垃圾收集器可以简化为的标记、标记终止、清除、清除终止这四个阶段,每个阶段有自己的任务,详情如下:

  • 标记阶段
    • GC状态切换至_GCMark,开启写屏障,根对象入队。

    • 标记任务和用户协程协同运行,用户协程协助一起标记活跃的内存对象(此时受混合写屏障保护,不会有内存安全问题)。

    • 使用分布式算法检查剩余工作,在标记阶段完成后进入标记终止阶段。

  • 标记终止阶段

    • 暂停程序,状态切换至_GCmarktermination,关闭辅助标记的用户协程。

    • 清理每个处理器上的线程缓存中的内容,以保证下一阶段没有悬挂的未标记对象(即未被标记为黑色的可达的对象)。

  • 清理阶段

    • 状态切换至_GCoff并开始清理阶段,初始化清理状态并关闭写屏障。

    • 用户协程恢复执行。

    • GC以后台并发的方式清理所有的未使用的内存单元(即未标记的白色对象) * 此时无需用户程序停止执行,清理和用户程序是并发的运行。

  • 清除终止阶段

    • 清理结束后,GC进入空闲状态(Idle),程序正常运行,等待下一次回收周期。

四、垃圾回收的触发时机

  go runtime 的GC整个过程是并发、增量的,它通过调用 "runtime.gcTrigger.test"函数,决定是否要执行垃圾回收,具体行为如下:

  • 通过一个“调步算法”,在堆空间内存分配到一定程度(总内存的一个区间内)时,再次申请内存(runtime.mallocgc函数)会触发GC。

  • 当堆内存达到上次GC时,内存分配的两倍时,触发新一轮的GC。

  • go runtime默认2分钟,进行定期触发。

  • 用户显性的通过runtime.GC()进行触发。

相关代码:https://github.com/golang/go/blob/01ba8bfe868df2eea10ea8dd5bfbe5af0549909d/src/runtime/mgc.go#L601

func (t gcTrigger) test() bool {if !memstats.enablegc || panicking.Load() != 0 || gcphase != _GCoff {return false}switch t.kind {case gcTriggerHeap:trigger, _ := gcController.trigger()return gcController.heapLive.Load() >= triggercase gcTriggerTime:if gcController.gcPercent.Load() < 0 {return false}lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))return lastgc != 0 && t.now-lastgc > forcegcperiodcase gcTriggerCycle:// t.n > work.cycles, but accounting for wraparound.return int32(t.n-work.cycles.Load()) > 0}return true
}

  最后总结一下,go runtime 通过内存对象三色抽象、混合写屏障、并发增量回收,用户协程协助等机制,共同将GC的暂停时间降至毫秒级别,为go语言成为高并发的代名词提供了重要的支持。

 

  

  

    

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/890641.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux怎么判断服务器的cpu架构

在部署应用程序和服务时,确认服务器的CPU架构是非常重要的,因为这会直接影响软件的兼容性和性能。在Linux系统中,有许多方法可以获取服务器的CPU架构信息。本篇文章将介绍几种常用的方法,并提供代码示例,帮助用户有效地获取这个信息。 1. CPU架构的概念 CPU架构是指中央处…

vscode中不同项目使用不用的nodejs版本

只需要在vscode中当前项目里面增加一个设置

低代码在项目管理中的5大实战案例:不懂代码也能快速搭建系统!

作为项目管理领域的“老司机”,我见过太多团队因传统开发效率低、需求响应慢而错失机会。低代码平台的崛起,让业务人员也能快速搭建系统,大幅缩短交付周期。以下是5个典型场景的实践案例,用最通俗的语言讲透核心逻辑👇案例1:3天上线CRM系统(客户关系管理) 背景:某销售…

[字符串算法]Manacher

我将永远追随六花的脚步1.前置知识 回文子串  回文的子串 最长回文子串  字符串中最长的回文子串 回文半径  设以\(i\)为中心的最大回文子串的长度为\(n\),则这个字符串第\(i\)位的回文半径为\((n+1)/2\) 2.算法流程 2.1 预处理 在处理回文子串(马拉车算法适用)的问题时…

[数据结构]树

我最喜欢六花了树(基础) 1 定义 1.1 树是什么 树是一种数据结构,因为形似倒着的树而得名. 树是一种特殊图 1.2 树的定义 递归定义 1.2.1 有根树的定义 形象化的,如图1,有根树存在根节点这一定义,从根节点可以分出任意个分支,这任意个分支又可以继续细分,分出的节点称…

StrokesPlus【电脑鼠标键盘手势软件】v0.5.8.0 中文绿色便携版

点击上方蓝字关注我 前言 StrokesPlus.net是一个超方便的手势识别软件,它能帮你用手势来代替鼠标和键盘操作。用起来既简单又灵活,功能还特别强大。 操作起来非常简单,它有好多实用的功能,比如智能识别你写的字、设定手势操作的区域、模拟鼠标的各种动作、运行脚本、响应窗…

大模型推理主战场:什么才是通信协议标配?

关键词:# DeepSeek ;# SSE ;# WebSocketSSE 和 WebSocket 是什么? 大模型应用出现前的主流网络通信协议是什么? 为什么大模型应用没有沿用 Web 类应用的主流通信协议? 为什么 SSE 和 WebSocket 更适合支持大模型应用? 实时通信协议的技术挑战和应对方案 Whats Next?Dee…

webSocket在.net中的使用案例

前言前面asp.net实现长连接 - chenxizhaolu - 博客园学习了如何在asp.net中实现http长连接,这里继续学习websocket。WebSockets 是一种协议,它能让客户端和服务器之间通过单个长期连接进行无缝通信。与 HTTP 等遵循请求-响应模式的传统网络通信方法不同,WebSockets 引入了全…

SQL SERVER日常运维巡检系列之-性能

前言做好日常巡检是数据库管理和维护的重要步骤,而且需要对每次巡检日期、结果进行登记,同时可能需要出一份巡检报告。本系列旨在解决一些常见的困扰:不知道巡检哪些东西 不知道怎么样便捷体检 机器太多体检麻烦 生成报告困难,无法直观呈现结果 性能是系统好坏的重要指标之…

burpsuite激活

激活burpsuite——教程点击Start 文件,把三个框都选上点击RUN,会自动启动,复制一下那个证书粘贴刚刚复制的密钥,点击下一个即可这里点击手动激活,复制请求,粘贴到刚刚那个激活程序的:Activation Request 它会自动生成Response,Copy就行到Burpsutie 里面复制一下,然后点…

KBP310-ASEMI整流桥稳定电力的核心担当

KBP310-ASEMI整流桥稳定电力的核心担当编辑:ll 在当今电子科技飞速发展的时代,各类电子设备充斥着我们的生活,从日常使用的手机、电脑,到工业生产中的大型机械,稳定的电力供应都是它们正常运转的基石。而在这背后,有一个常常被忽视却又至关重要的元件 ——KBP310 整流桥。…

GraphQL开发工具选型指南:Apipost高效调试与文档生成实战解析

GraphQL 调试与文档生成:Apipost 如何简化开发流程 GraphQL开发工具选型指南:Apipost高效调试与文档生成实战解析 GraphQL 凭借其灵活的数据查询能力和高效的接口设计,是现代 API 开发的主流选择。根据 State of JS 2022 的调研,GraphQL 在开发者中的采用率已超过 40%,尤其…