这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
Go 语言内存管理及优化
堆和栈的了解
-
堆:用于动态分配内存,基本上是由程序员申请分配和释放。他是从低地址位向高地址位增长,采用的是链式存储结构。
-
栈:是编译器自动释放,存放函数的参数值,每当一个函数被调用的时候 函数就的返回类型和一些调用信息就会被放置到栈中,栈是高地址位向低地址位增长,是连续的内存区域。
堆和栈的区别:
- 堆内存储存的是实体(如对象)然而栈储存的是局部变量
- 栈的内存的更新的速度快于堆内存的更新速度(原因栈是储存局部变量)
- 栈的内存存放的变量周期一旦结束就会被释放掉,栈的内存存放的是实体所以结束了之后不会被立即释放掉 需要垃圾回收器不定时回收释放
Golang 的内存分块
- 目标:为对象在 heap 上分配内存
- 提前将内存分块
- 调用系统调用 mmap() 向 OS 申请一大块内存,例如 4 MB
- 先将内存划分成大块,例如 8 KB,称作 mspan
- 再将大块继续划分成特定大小的小块,用于对象分配
- noscan mspan: 分配不包含指针的对象 —— GC 不需要扫描
- scan mspan: 分配包含指针的对象 —— GC 需要扫描
- 对象分配:根据对象的大小,选择最合适的块返回
Golang 的缓存机制
- TCMalloc: thread caching
- 每个 p 包含一个 mcache 用于快速分配,用于为绑定于 p 上的 g 分配对象
- mcache 管理一组 mspan
- 当 mcache 中的 mspan 分配完毕,向mcentral 申请,带有未分配块的 mspan
- 当 mspan 中没有分配的对象,mspan 会被缓存在 mcentral 中,而不是立刻释放并归还给 OS
Golang 的内存分配存在的问题
- 对象分配是非常高频的操作:每秒分配 GB 级别的内存
- 小对象占比较高
- Go 内存分配比较耗时
- 分配路径长:g -> m -> p -> mcache -> mspan -> memory block -> return pointer
- pprof:对象分配的函数是最频繁调用的函数之一
Balanced GC 对内存分配的优化
- 每个 g 都绑定一大块内存(1KB),称作 goroutine allocation buffer (GAB)
- GAB 用于 noscan 类型的小对象分配:< 128 B
- 使用三个指针维护 GAB: base, end, top
- Bump pointer(指针碰撞)风格对象分配
- 无须和其他分配请求互斥
- 分配动作简单高效
问题
GAB 对于 Go 内存管理来说是一个大对象,因此这种分配方式本质是将多个小对象的分配合并成一次大对象的分配,因此存在的问题是 GAB 的对象分配方式会导致内存被延迟释放。因此,当 GAB 中哪怕只有一个小对象存活时,Go runtime 也会认为整个大对象(即 GAB)存活。
Balance GC 的解决方案
- 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中
- 原来的GAB可以释放,避免内存泄漏
本质:用 copy GC 的算法管理小对象,balanced GC 会根据 GC 策略,将 GAB 中存活的对象移动到另外的 GAB 中,从而压缩并清理 GAB 的内存空间,原先的 GAB 空间由于不再有存活对象,可以全部释放,如下图所示。