这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
Go的内存分配
Golang的内存分配是指为对象在heap上分配内存,go提前将内存分成了多个小块,来了一个对象就选择一个最接近的内存块
两个思想
go内存分配有两个思想:分块和缓存
分配过程
- 调用系统==mmap()== 向操作系统申请一大块内存,例如4MB
- 先将内存划分成大块,例如8KB,称作==mspan==
- 再把大块继续划分成特定大小的小块,用于对象分配
- noscan mspan: 分配不包含指针的对象,也就是说对于GC而言不需要扫描
- scan mspan: 与前一条相反,分配包含指针的对象,对于GC而言需要扫描
对于这部分,就是分块思想的应用
go的内存分配借鉴了TCMalloc,TC指的是Thread Caching,也就是对于内存进行了多级的不同缓存,从而加快内存分配的速度
分配内存就如这张图所示,从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)风格对象分配,它具有如下优点
- 无须和其他分配请求互斥
- 分配动作简单高效
这张图就可以相对清晰的表示出优化方案的细节,其实就是提供一个base和end表示这个内存块的位置,而top则表示这个内存块中当前使用量
一个GAB对于Go内存管理来说是一个大对象(对,就是一整个对象),因此GAB的本质是将多个小对象的分配合并成一次大对象的分配
但是也会产生问题:GAB的对象分配方式会导致内存被延迟释放,也就是说,是拿内存换时间了
这里,字节提供了一个方案:移动GAB中的存活对象
- 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中
- 原先的GAB可以释放,避免内存泄漏
这个方案的本质是用coping GC的算法管理小对象,也就是根据对象的生命周期,使用不同的标记和清理策略
个人认为这样其实就是用空间换时间,在分配内存上再加一层自定义管理层。
这种优化是在语言层面的优化,并不是针对特定业务