GO的内存管理

​ 这其实和上一篇文章“GMP调度模型”有着一定关联,当一个协程通过GMP模型调度产生对象以后,就要涉及到Go的内存管理机制了。本文的内存管理是一个比较大的概念,包含了垃圾分配对象内存管理

0 整体架构&概念

​ 首先要明确自动内存管理其实就是指垃圾回收,由程序语言的运行时系统(runtime)管理动态内存。另外,几个英文对照的概念:

  • Mutator:业务线程,分配新对象,修改对象指向关系
  • Collector:GC线程,找到存活对象,回收死亡对象的内存空间
  • allocator:内存分配器,应用需要内存都要向allocator申请
  • Serial GC:只有一个collector
  • Parallel GC:支持多个collectors同时回收的GC算法
  • Concurrent GC:mutator(s)和collector(s)可以同时执行

image-20220519174137814.png

1 Go垃圾回收

​ 首先,垃圾回收主要可以从以下方面讨论:

  • 垃圾分类
  • 标记流程
  • 清理过程

1.1 垃圾分类

​ 语法垃圾和语义垃圾

语义垃圾

​ 这是垃圾回收器无法回收的垃圾,通常是由于程序员的不正当操作导致的内存泄露而产生的。

image-20220519184026434.png

​ 如图,数组后面的两个元素无法再访问了,但其关联的堆上内存依然是无法释放的。

语法垃圾

​ 也就是指针指向关系不可达的对象,是垃圾回收的对象

1.2 标记流程

​ 众所周知,Go使用的标记算法是三色标记法。那为什么不适用引用计数法呢?由于其在并发时不可扩展,对于Go这样的高并发语言并不适合。

​ 在以下文章总结了Go不同版本的标记算法,图文精炼,故我就不写了= =,真不是懒QAQ

​ [典藏版]Golang三色标记、混合写屏障GC模式图文全分析 - SegmentFault 思否

1.3 清理过程

​ 相对于标记流程,对象的清理和内存释放就简单多了

​ 进程启动时会有两个特殊 goroutine:

  • 一个叫 sweep.g,主要负责清扫死对象,合并相关的空闲页
  • 一个叫 scvg.g,主要负责向操作系统归还内存

​ 当GC的标记流程结束,就会启动sweep.g,进行清扫工作。之后scvg.g被唤醒,执行线性流程,将页内存归还给操作系统。

​ 为什么Go不使用分代GC?

​ 由于分代假说假定大部分变为垃圾的对象都是新创建的对象,但是在Go语言中由于编译器的优化,通过内存逃逸的机制,将会继续使用的对象转移到了堆中。大部分新创建的对象很快变为垃圾的对象会在栈中分配。这和其他使用隔代GC的编程语言有显著的不同,这减弱了使用隔代GC的优势。同时, 隔代GC需要额外的写屏障来保护并发垃圾回收时对象的隔代性,这会减慢GC的速度。因此,隔代GC是被尝试过并抛弃的方案

2 Go内存分配

2.1 分块

目标:为对象在heap上分配内存

将内存分块:

  • 调用系统调用mmap(),向OS申请一大块内存,例如4MB
  • 先将内存划分成大块,例如8KB,称为mspan
  • 再将大块继续划分为特定大小的小块,用于对象分配
  • noscan mspan:分配不包含指针的对象——GC不需要扫描
  • scan mspan:分配包含指针的对象——GC需要扫描

对象分配:根据对象的大小,选择最合适的块返回

2.2 缓存

​ 使用了TCMalloc18张图解密新时代内存分配器TCMalloc (qq.com)

image-20220519193858815.png

  • 每个p包含一个mcache用于快速分配,用于为绑定于p上的g分配对象
  • mcache管理一组mspan
  • mcachemspan分配完毕,向mcentral申请带有未分配块的mspan
  • mspan中没有分配的对象,mspan会被缓存在mcentral中,而不是立刻释放并归还给OS

2.3 字节的优化方案

​ 每个g都绑定一大块内存(1KB),称为goroutine allocation buffer(GAB)

​ GAB用于noscan类型的小对象分配:<128B

​ 使用三个指针维护GAB:base,end,top

​ Bump pointer(指针碰撞)风格对象分配

  • 无须和其他分配请求互斥
  • 分配动作简单高效

image-20220522202648374.png

​ 如图,只需要移动指针就可以完成对象的分配

存在的问题

  • GAB对于Go内存管理是一个大对象
  • 本质:将多个小对象的分配合并成一个大对象的分配
  • 问题:GAB的对象分配方式会导致内存被延迟释放
  • 方案:移动GAB中存活的对象
    • 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配GAB中
    • 原先的GAB可以释放,避免内存泄露
    • 本质:用copying GC的算法管理小对象

image-20220522210831818.png

3 总结

​ Go的内存管理包括了垃圾回收和内存分配,之前有看过Java这方面的知识,感觉还是挺容易理解的,并且Go没有垃圾回收器,TLAB等概念,感觉八股这块还是略弱于Java哈哈。

参考

图解 Go GC (qq.com)

字节跳动 Go 语言面试高频题 01:内存分配 - 知乎 (zhihu.com)

Go垃圾回收系列之二:标记准备 - 开发者头条 (toutiao.io)


GO的内存管理
https://2w1nd.github.io/2022/05/22/Go/GO的内存管理/
作者
w1nd
发布于
2022年5月22日
许可协议