GO的内存管理

259 阅读4分钟

当一个协程通过GMP模型调度产生对象以后,就要涉及到Go的内存管理机制了。本文的内存管理是一个比较大的概念,包含了垃圾分配对象内存管理。 ​ 首先要明确自动内存管理其实就是指垃圾回收,由程序语言的运行时系统(runtime)管理动态内存。另外,几个英文对照的概念:

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

 Go垃圾回收

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

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

1.1 垃圾分类

​ 语法垃圾和语义垃圾

语义垃圾

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

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

1.2 标记流程

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

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

1.3 清理过程

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

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

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

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

2 Go内存分配

2.1 分块

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

将内存分块:

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

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

2.2 缓存

  • 每个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(指针碰撞)风格对象分配

  • 无须和其他分配请求互斥
  • 分配动作简单高效 如图,只需要移动指针就可以完成对象的分配

存在的问题

  • GAB对于Go内存管理是一个大对象

  • 本质:将多个小对象的分配合并成一个大对象的分配

  • 问题:GAB的对象分配方式会导致内存被延迟释放

  • 方案:移动GAB中存活的对象

    • 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配GAB中
    • 原先的GAB可以释放,避免内存泄露
    • 本质:用copying GC的算法管理小对象

    3 总结

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