这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
课程重点
- 了解自动内存管理
- Go内存管理及优化
1.了解自动内存管理
- 自动内存管理(GC)
所谓自动内存管理,管理的是程序中按需求动态分配的内存部分
自动内存管理可以方便程序员在写代码时专注于实现业务逻辑而不是分配精力进行内存管理
自动内存管理要保证内存使用的正确性和安全性,它主要有三个任务:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
- 自动内存管理的相关概念:
Mutator:业务线程,分配新对象,修改对象指向关系
Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
Serial GC: 只有一个 collector
Parallel GC: 支持多个 collectors 同时回收的 GC 算法
Concurrent GC: mutator(s) 和 collector(s) 可以同时执行
- 评价 GC 算法:
安全性 (Safety): 不能回收存活的对象,是基本要求
吞吐率(Throughput): 花在 GC 上的时间(越长吞吐率越小)
暂停时间(Pause time): stop the world (STW) 业务是否感知
内存开销 (Space overhead) GC元数据开销
追踪垃圾回收(Tracing garbage collection)
引用计数(Reference counting)
在垃圾回收的过程中,一般会经历如下过程:
对象被回收的条件: 指针指向关系不可达的对象
-
标记根对象
静态变量、全局变量、常量、线程栈等
-
标记: 找到可达对象
求指针指向关系的传递闭包: 从根对象出发,找到所有可达对象
- 清理: 所有不可达对象
- Copying GC
将存活对象复制到新的内存区域
- Mark-sweep GC
将死亡对象占用的空间标记为可用空间
- Mark-compact GC
原地整理存活对象,将他们集中在一起
根据对象的生命周期,使用不同的标记和清理策略
为了给不同的对象分配合适的清理方式,GC引入了分代假说:
- 每个对象都有年龄:经历过GC的次数
- 目的:对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销
- 不同年龄的对象处于heap的不同区域
-
年轻代(Young generation)
-
常规的对象分配
-
由于存活对象很少,可以采用 copying collection
-
GC 吞吐率很高
-
-
老年代(Old generation)
-
对象趋向于一直活着,反复复制开销较大
-
可以采用 mark-sweep collection
-
在内存管理中,还有另外一种方法 —— 引用计数
- 每个对象都有一个与之关联的引用数目
- 对象存活的条件: 当且仅当引用数大于 0
- 优点:
- 内存管理的操作被平摊到程序执行过程中
- 内存管理不需要了解 runtime 的实现细节: C++ 智能指针(smart pointer)
- 缺点
- 维护引用计数的开销较大: 通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形数据结构 - weak reference
- 内存开销:每个对象都引入的额外内存空间存储引用数目
- 回收内存时依然可能引发暂停
- Go内存管理及优化
Go语言在分配内存时使用的是分块机制,主要有以下几个流程:
目标:为对象在 heap 上分配内存
- 提前将内存分块
- 调用系统调用 mmap() 向 OS 申请一大块内存,例如 4 MB
- 先将内存划分成大块,例如 8 KB,称作 mspan
- 再将大块继续划分成特定大小的小块,用于对象分配
- noscan mspan:分配不包含指针的对象-GC 不需要扫描
- scan mspan: 分配包含指针的对象-GC 需要扫描
- 对象分配:根据对象的大小,选择最合适的块返回
为了进行高效的内存分配,Go还为此配置了缓存
- 每个 p 包含一个 mcache 用于快速分配,用于为绑定于 p 上的 g 分配对象
- 每个 mcache 管理一组 mspan
- 当 mcache 中的 mspan 分配完毕,向 mcentral 申请带有未分配块的 mspan
- 当 mspan 中没有分配的对象,mspan 会被缓存在mcentral 中,而不是立刻释放并归还给 OS
在我们了解了Go的内存机制之后,我们应该怎样进行对应优化呢?
首先,我们需要了解现有机制的问题:
- 对象分配是非常高频的操作: 每秒分配 GB 级别的内存
- 内存分配过程中小对象占比较高
- Go 内存分配比较耗时:
- 分配路径长: g -> m -> p -> mcache -> mspan -> memory block -> return pointer
- 对象分配的函数是最频繁调用的函数之一
优化方案:
- Balanced GC
将许多小对象分配到同一个内存空间g中,使用指针地址进行内部小对象的内存分配
这种操作无须和其他分配请求互斥,并且分配动作简单高效
但是这样的内存分配也有弊端,在进行内存回收时:
同一个空间会被Go的内存管理看作一个大的对象,只要其中一个小对象存活,则整体存活
这样就会导致一卡大内存空间中只有一小部分由于,造成浪费
改进方案:
使用Copying GC算法进行小对象内存管理,将存活的对象拷贝到另一个内存空间中