内存管理
Go 中,内存分配主要有两个思想:
- 分块
- 缓存
分块
分块的思路为:
- 调用系统调用
mmap()
向 OS 申请一大块内存,例如 4MB。 - 将内存划分为大块,例如 8KB,称为 mspan。
- noscan mspan:分配不包含指针的对象,GC 不需要扫描。
- scan mspan:分配包含指针的对象,GC 需要扫描。
- 再将 mspan 划分为特定大小的小块,用于对象分配。
缓存
缓存的基本思路是:
- 每一个 P 包含一个 mcache 用于快速分配,mcache 管理一组 mspan。
- 当 mcache 中的 mspan 分配完毕,向 mcentral 申请带有未分配块的 mspan。
- 当 mspan 中没有分配的对象,mspan 会被缓存在 mcentral 中,而不是立刻释放归还给 OS。
优化 - Balanced GC
字节跳动有自己的 Go 语言内存管理优化方案,即 Balanced GC,其思路如下:
每个 G 都绑定一大块内存(1 KB),称为 Goroutine Allocation Buffer(GAB)。
- GAB 用于 noscan 类型的小对象(小于 128B)的分配。
- GAB 使用三个指针进行维护:base、end、top。使用指针碰撞风格进行对象分配。
- GAB 对于 Go 内存管理来说就是一个大对象。
GAB 有一个问题:会导致内存被延迟释放,GAB 中即使只有一个很小的对象存活,Go 内存管理也不会回收其余空闲空间。
解决方法:
当 GAB 中存活对象大小少于一定阈值时,将 GAB 中存活的对象复制到另外分配的 GAB(Survivor GAB)中,原先的 GAB 可以释放。
编译器优化
函数内联
函数内联是指,将被调用函数的函数体的副本,替换到调用位置上,同时重写代码以反映参数的绑定。
优点:
- 消除函数调用的开销,如传递参数、保存寄存器等。
- 扩展了函数边界,更多对象不逃逸分析。
缺点:
- 函数体变大。
- 编译生成的可执行文件变大。
逃逸分析
逃逸分析步骤:
- 从对象分配处出发,观察对象的数据流。
- 若发现指针 p 在当前作用域 s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他 goroutine
- 传递给已逃逸的指针指向的对象
- 则指针 p 指向的对象逃逸出 s,反正没有逃逸。
对于未逃逸的对象,在栈上分配;逃逸对象,在堆上分配。