自动内存管理&内存管理与优化 | 青训营笔记
这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
自动内存管理
概念
-
动态内存
- 程序在运行时根据需求动态分配的内存:malloc()
-
自动内存管理(垃圾回收)
- 由程序语言的运行时系统回收动态内存
- 避免手动内存管理,专注于实现业务逻辑
- 保证内存使用的正确性和安全性
-
三个任务
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
-
Mutator:业务线程,分配新对象,修改对象指向关系
-
Collector:GC线程,找到存活对象,回收死亡对象的内存空间
-
Serial GC:只有一个collector。执行时业务线程必须暂停。
-
Parallel GC:支持多个collectors同时回收的GC算法。执行时业务线程必须暂停。
-
Concurent GC:mutator(s)和collector(s)可以同时执行
- 前提:Collectors必须感知对象指向关系的改变,例如已标记对象指向的对象必须被标记。
评价GC算法优劣的指标:
- 安全性:不能回收存活的对象基本要求
- 吞吐率: 花在GC上的时间
- 暂停时间:业务是否感知
- 内存开销:GC元数据开销
GC算法
分代GC概念
- 分代假说,很多对象在分配出来后很快就不再使用了
- 每个对象都有年龄:经历过GC的次数
- 目的:对年轻和老年的对象,指定不同的GC策略,降低整体内存管理的开销
- 不同年龄的对象处于heap的不同区域
追踪垃圾回收
对象被回收的条件:指针指向关系不可达的对象。
感觉有点像并查集,又有点像floyd求传递闭包。
- 标记根对象
- 静态变量、全局变量、常量、线程栈等
- 从根对象,标记所有可达的对象(求一个传递闭包)
- 清理所有不可达的对象
- 将存活对象复制到另外的内存空间(Copying GC)
- 或者将死亡对象的内存标记为可分配(Mark-sweep GC)
- 或者移动并整理存活对象(有点像OS的那个清理碎片的感觉,把小碎片集合在一起变成大碎片)(Mark-compact GC)
根据对象的声明周期,使用不同的标记和清理策略:
- 年轻代
- 常规的对象分配
- 由于存活对象很少,可以采用copying colliction
- GC吞吐率很高
- 老年代
- 对象倾向于一直活着,反复复制开销较大
- 可以采用
mark-sweep collection
引用计数
每个对象都有一个与之关联的引用数目,对象存活的条件为当且仅当引用数大于0。
听着就是拓扑排序!只不过拓扑排序是有方向的。
优点:
- 内存管理的操作被平摊到程序执行过程中
- 内存管理不需要了解runtime的实现细节
缺点:
- 维护引用计数的开销较大;通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形数据结构(和拓扑一样)
- 内存额外开销,每个对象都要引入额外的内存空间来存储引用数目
- 回收内存时间依然可能引发暂停
内存管理及优化
内存分配
分块
目标:为对象在heap上分配内存。
提前将内存分块:
- 调用系统调用
mmap()向OS申请一大块内存,例如4MB - 先将内存划分成大块,例如8KB,称作
mspan - 再将大块继续划分成特定大小的小块,用于对象分配
noscan mspan,分配不包含指针的对象,也就是GC不需要扫描scan mspan,分配包含指针的对象,GC需要扫描
对象分配的原则是根据对象的大小,选择最合适的块返回
缓存
原理好像是TCMalloc,thread caching。
- 每个p包含一个mcache用于快速分配和绑定与p上的g分配对象
- mcache管理一组mspan
- 当mcache中的mspan分配完毕,向mcenteral申请带有未分配块的mspan
- 当mspan中没有分配的对象,mspan会被缓存在mcentral中,而不是立刻释放并归还给OS
内存管理优化
内存管理优化的重要性:
- 对象分配是非常高频的操作:每秒分配GB级别的内存
- 小对象占比较高
- Go内存分配比较耗时
一种可行的优化方案:Balanced GC
- 每个g都绑定一大块内存(1KB),称作
goroutine allocation buffer(GAB) - GAB用于noscan类型的小对象分配:<128B
- 使用三个指针维护GAB:base, end, top
- 分配简单高效
if top + size <= end {
addr := top
top += size
return addr
}