Golang的内存管理优化 | 青训营笔记

233 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

Go的内存分配

Golang的内存分配是指为对象在heap上分配内存,go提前将内存分成了多个小块,来了一个对象就选择一个最接近的内存块

两个思想

go内存分配有两个思想:分块和缓存

分配过程

  • 调用系统==mmap()== 向操作系统申请一大块内存,例如4MB
  • 先将内存划分成大块,例如8KB,称作==mspan==
  • 再把大块继续划分成特定大小的小块,用于对象分配
  • noscan mspan: 分配不包含指针的对象,也就是说对于GC而言不需要扫描
  • scan mspan: 与前一条相反,分配包含指针的对象,对于GC而言需要扫描

对于这部分,就是分块思想的应用

go的内存分配借鉴了TCMalloc,TC指的是Thread Caching,也就是对于内存进行了多级的不同缓存,从而加快内存分配的速度

image-20230123225732535

分配内存就如这张图所示,从g->m->p->...逐层的找合适的内存块,一直到重新申请一块内存

当然go也会利用一定的策略吧内存块还给操作系统

这一部分,就是golang缓存的思想体现

golang内存分配机制存在的问题

go的内存分配机制存在这些问题:

  • 对象分配是非常高频的操作,每秒可能会分配GB级别的内存
  • 在对象分配过程中,大部分对象都是小对象(绝大多数对象都小于80B)
  • Golang内存分配比较耗时
    • 分配路径较长,根据上图可以知道,我们分配可以是:g->m->p->mcache->mspan->memory block->return pointer,太长了,而且我们高频率的调用对象分配,导致我们在对象分配上消耗了太多的CPU性能

字节提供的优化方案:Balanced GC

  • 每个g都绑定一大块内存(比如说1KB),称作==goroutine allocation buffer(GAB)==
  • 我们在GAB里去作用于noscan类型的小对象分配(通常小于128B)
  • 我们使用三个指针来维护GAB,分别是base, end, top
  • 有了GAB后,我们可以在GAB上做指针碰撞(Bump Pointer)风格对象分配,它具有如下优点
    • 无须和其他分配请求互斥
    • 分配动作简单高效

image-20230123230513096

这张图就可以相对清晰的表示出优化方案的细节,其实就是提供一个base和end表示这个内存块的位置,而top则表示这个内存块中当前使用量

一个GAB对于Go内存管理来说是一个大对象(对,就是一整个对象),因此GAB的本质是将多个小对象的分配合并成一次大对象的分配

但是也会产生问题:GAB的对象分配方式会导致内存被延迟释放,也就是说,是拿内存换时间了

这里,字节提供了一个方案:移动GAB中的存活对象

  • 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中
  • 原先的GAB可以释放,避免内存泄漏

这个方案的本质是用coping GC的算法管理小对象,也就是根据对象的生命周期,使用不同的标记和清理策略

个人认为这样其实就是用空间换时间,在分配内存上再加一层自定义管理层。

这种优化是在语言层面的优化,并不是针对特定业务