内存管理
第一个思想:分配
-
目标:为对象在heap上分配
-
go的做法:提前将内存分块,要用的时候填入尺寸最接近的块
-
具体做法
-
系统调用 mmap() 向 OS申请了一大块内存(例如4mb)
-
先将内存划分成大块,例如 8kb ,称为 mspan
-
在将大块继续划分成特定大小的小块,用于对象分配
-
分类
- noscan mspan:分配不包含指针的对象 (GC不需要扫描)
- scan mspan:包含指针的对象 (GC需要扫描)
-
第二个思想:缓存
- 通过 TCMalloc(thread caching) 内存分配器来分配
- 每个 p 包含一个 mcache 用于快速分配,用于为绑定于 p 上的 g 分配对象
- gorutine 出发找到m后找到p,在p上的 mcache 中存放了一组 mspan,每个 mspan 的大小是不一样的,根据对象大小找到最合适的 mspan 中的空位后返回出去,就完成了一次对象的分配
- 当我们发现 mcache 中的 mspans 都是满的,我们就需要到下一个级别的缓存 mcentral 中找一个带有空余对象的 mspan,将此 mspan 填入 mcache,再将这个 mspan 中的一个空位返回出去
- 结论:go做了多层缓存让我们更快地将对象分配出去
- 当 mspan 中没有对象时,mspan 会被缓存在 mcentral 中,而不是立刻释放并归还给 OS
优化
-
对象分配是非常高频的操作,每秒都能分配几个G的内存
-
小对象的占比更高
-
Go 内存分配比较耗时
- 分配路径长:g -> m -> p -> mcache -> mspan -> memory block -> return pointer
- pprof:对象分配的函数是最频繁调用的函数之一(cpu消耗高)
优化方案:Balanced GC
-
每个 g 都绑定一大块内存(1kb),称为 goroutine allocaton buffer(GAB)
-
GAB 用于 noscan 类型的小对象分配: <128 B
-
使用三个指针维护 GAB :base,end,top
-
使用 Bump pointer(指针碰撞)风格对象分配
- 无需和其他分配请求互斥
- 分配动作简单高效
-
-
技术细节:
-
GAB 对于 Go 内存管理来说是一个大对象
-
本质:将多个小对象的分配合并成一个大对象的分配
-
问题:GAB 的对象分配方式会导致内存被延迟释放(一个小对象的存活会导致一个大对象一直存活,浪费内存资源)
-
方案:移动 GAB 中的存活对象
- 当 GAB 的总大小超过阈值时,将 GAB 中存活的对象复制到另外分配的 GAB 中
- 原先的 GAB 可以释放,避免内存泄漏
- 本质:用 copying GC 的算法对内存进行清理
-