这是我参与「第五届青训营」伴学笔记创作活动的第 5 天
1. 概述
我将主要介绍如下知识点:
- Go 语言自动内存管理
- Go 语言内存分配
- Go 语言内存管理优化
2. 自动内存管理
2.1 概念
-
动态内存
- 程序在运行时根据需求动态分配的内存:类似于C语言的
malloc()
- 程序在运行时根据需求动态分配的内存:类似于C语言的
-
自动内存管理(垃圾回收)
- 由程序语言的运行时系统管理动态内存
- 避免手动内存管理,专注于实现业务逻辑
- 保证内存使用的正确性和安全性,防止出现
double-free problem,use-after-free problem
-
垃圾回收(GC)的三个任务
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
其它相关概念补充:
-
Mutator:业务线程,分配新对象,修改对象指向关系
-
Collector:GC 线程,找到存活对象,回收死亡对象的内存空间
-
Serial GC(串行 GC):只有一个 collector
-
Parallel GC(并行 GC):支持多个 collectors 同时回收的 GC 算法
-
Concurrent GC(并发 GC):mutator(s) 和 collector(s) 可以同时执行
-
Collectors 必须感知对象指向关系的改变
-
2.2 追踪垃圾回收(Tracing garbage collection)
对象被回收的条件:指针指向关系不可达的对象
过程
-
标记根对象(GC root)- 静态变量,全局变量,常量,线程栈等
-
标记:找到可达对象- 求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
-
清理:回收所有不可达对象占据的空间,不同清理策略如下:- 将存活对象复制到另外的内存空间(Copying GC)
- 将死亡对象的内存标记为 "可分配" (Mark-sweep GC)
- 移动并整理(原地整理)存活对象(Mark-compact GC)
注:根据对象的生命周期,使用不同的标记和清理策略
2.3 分代 GC(Generational GC)
-
分代假说:most objects die young
-
Intuition:很多对象在分配出来后很快就不再使用了
-
每个对象都有年龄:经历过GC的次数
-
目的:对年轻和老年的对象,指定不同的GC策略,降低整体内存管理的开销
-
不同年龄的对象处于 heap 的不同区域
-
年轻代(Young generation)- 常规的内存分配
- 由于存活对象很少,可以采用 copying GC
- GC吞吐率很高
-
老年代(Old generation)- 对象趋向于一直活着,反复复制开销较大
- 可以采用 mark-sweep GC
2.4 引用计数(Reference counting)
-
每个对象都有一个与之关联的引用数目
-
对象存活的条件:当且仅当引用数大于0
-
优点:
- 内存管理的操作被平摊到程序执行过程中
- 内存管理不需要了解 runtime 的实现细节:C++智能指针(smart pointer)
-
缺点:
- 维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形数据结构——weak reference
- 内存开销:每个对象都引入的额外内存空间存储引用数目
- 回收内存时依然可能引发暂停
3. 内存分配
3.1 分块
-
目标:为对象在 heap 上分配内存
-
提前将内存分块
- 调用系统调用
mmap()向 OS 申请一大块内存,例如 4 MB - 先将内存划分成大块,例如 8 KB,称作
mspan - 再将大块继续划分成特定大小的小块,用于对象分配
noscan mspan:分配不包含指针的对象—— GC 不需要扫描scan mspan: 分配包含指针的对象 —— GC 需要扫描
- 调用系统调用
-
对象分配:根据对象的大小,选择最合适的块返回
3.2 缓存
Go 内存管理构成了多级缓存机制,从 OS 分配得的内存被内存管理回收后,也不会立刻归还给 OS,而是在 Go runtime 内部先缓存起来,从而避免频繁向 OS 申请内存
- 每个 p 包含一个 mcache 用于快速分配,用于为绑定于 p 上的 g 分配对象
- mcache 管理一组 mspan
- 当 mcache 中的 mspan 分配完毕,向 mcentral 申请带有未分配块的 mspan
- 当 mspan 中没有分配的对象,mspan 会被缓存在 mcentral中,而不是立刻释放并归还给OS
4. 内存管理优化
4.1 问题分析
-
对象分配是非常高频的操作:每秒分配 GB 级别的内存
-
小对象占比较高
-
GO 内存比较耗时
- 分配路径长:g -> m -> p -> mcache -> mspan -> memory block -> return pointer
- pprof:对象分配的函数是最频繁调用的函数之一
4.2 优化方案
字节跳动的优化方案:Balanced GC
- 指针碰撞风格的对象分配
- 实现了 copying GC
- 性能收益
详细内容可自行查阅
5. 总结
学术界和工业界一直在致力于解决自动内存管理技术的不足之处
参考:
- 字节内部课:GO发行版优化与落地实践