这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
自动内存管理
概念
- 动态内存
- 程序在运行时根据需求动态分配的内存:malloc()
- 垃圾回收:有程序语言的运行时系统管理动态内存。
- 避免手动回收,专注业务实现
- 保证内存是安全性和正确性。
- Mutator:业务线程,分配新对象,修改对象指向关系,也就是用户启动的线程。
- Collector:GC线程,找到存活对象,回收垃圾对象。
- Seruak GC: 只有一个collector。
- Parallel GC: 支持多个collectors 同时回收的GC算法。
- Concurrent GC: mutator(s) 和 collector(s) 可以同时执行。
Concurrent GC 遇到的挑战
Concurrent GC 是并发标记,标记的时候用户线程和 GC 线程同时进行。
所以当回收时遇到以下图中情况,第一张图是GC前,O、a都没有被标记。第二张图,Collectors 先将 O、a 标记为存活。
但这时,GC 还没结束,用户就添加了一个新的引用关系 o 引用 b。
这时Collectors 已经标记完 O、a 这一串,但是必须感知到新增的引用关系,去处理它,不能漏掉对它的GC。
GC 的三个任务
- 为新对象分配空间
- 找到存活对象
- 回收垃圾对象
GC 的评价标准
- 安全性
- 不能回收存活的对象
- 吞吐率
- 1 - GC时间/程序执行总时间
- 暂停时间
- 业务是否感知到
- 内存开销
- GC内存开销。
常见方式
追踪垃圾回收算法
回收对象
指针指向关系不可达对象
流程
- 标记根对象
- 常量、变量、静态变量、常量。
- 找到可达对象
- 清理不可达对象
- 标记整理
- 标记清除
- 标记复制
怎么选择合适的回收算法
根据生命周期来选择
-
老年代使用标记-整理算法。
-
新生代使用标记-清除 + 标记-整理算法。
引用计数垃圾回收算法
回收对象
- 被引用数不大于0的对象
优点
- 垃圾回收操作平摊到程序执行过程中,不像追踪垃圾回收算法,每次回收时才扫描一遍,引用计数回收算法,在程序运行时,就维护着计数。
- 内存管理不需要了解 runtime 的实现细节,维护计数就行,比如C++智能指针,一个库就能实现垃圾回收。
缺点
- 引用计数统计需要具备原子性,开支大
- 没办法回收环形数据结构
- 每个对象都要开辟空间,用来计数
- 回收时依然有stopworld
Go内存管理及优化
内存分配策略
提前将内存分区
- 调用系统调用 mmap() 分配一块4 MB 的内存。
- 将内存分大块,每一块 8KB,称作 mspan。
- 再将大块划分成特定大小的小块,用于对象分配。
最后将 mspan 划分为两类
- noscan mspan: 分配不包含指针的对象 -- GC 不用扫描
- scan mspan: 分配包含指针的对象 -- GC 需要扫描
分配
根据对象的大小,找到最合适的直接分配。
内存分配 --多级缓存
简介
go 实现了内存分配的多级缓存机制。
对于没有对象的 mspan(空闲内存),不会立即回收,而是进行缓存,从而加快内存分配。
原理
- 图中 g 指的是 goroutine,每个 p 都指向一个 mcache 来为 groutine 分配内存。
- 当分配对象时,首先在 mache 中查找有无合适的 mspan。
- 找不到合适的 mspan(或者 mspan 都满了),就去查询下级 mcentral 有无合适的 mspan,有的话,直接拿到
mcache 中使用。
- 当 mspan 没有分配的对象,msapn 会被缓存在 mcentral 中,而不是立刻释放。
- 这就尽可能保证 mcentral 中尽量有空闲内存。
内存管理优化
问题
- 对象分配是高频操作:每秒分配GB级别内存。
- 小对象占比高
- Go 内存分配比较耗时
- 分配链路长: g -> m -> p -> mcache -> mspan -> memory block -> return pointer。
- pprof: 对象分配中调用最频繁的函数。
字节优化方案:Balanced GC
简介
为每个 goroutine 绑定一个 1kb 的空间(GAB)(goroutine allocation buffer)。
并规定小于 128b 的小对象都在这个 GAB 中直接分配。
当 GAB 用完后,通过 mcache 再次申请内存。
这样就把小对象内存分配转化成了大对象内存分配,极大的提高效率。
GAB 内存分配算法
GAB 中有 base 指针指向 GAB 起始位置,end 指针指向 GAB 结束位置。
top 指针从 base 开始,已分配内存与未分配内存区域的边界线。
GAB 中分配内存只需要移动 top 指针就好。
简单高效。
优点
- GAB 是线程独有的,不需要考虑并发问题。
- 分配动作(移动top指针)简单高效。
缺点
描述
- GAB 内部小对象存活,导致整个 GAB 大对象无法回收。
- Go 采用的引用追踪内存回收算法,当小对象存活时,会依赖于 GAB 大对象,导致整个 GAB 无法被回收。
解决方案
- (标记-复制)当 GAB 中对象总大小到某个阈值的时候,分析存活对象,并将存活对象复制到新的 GAB 中,
Balanced GC 收益
高峰期 CPU useage 降低 4.6%,核心接口时延下降 4.5% - 7.7%。