Go - GC
逃逸分析
- 在传统的不带 GC 的编程语言中,需要关注对象的分配位置,是分配在
堆上还是栈上 - Go 集成了
逃逸分析功能来自动判断对象是应该分配在堆上还是栈上- 只有在
代码优化时,才需要研究具体的逃逸分析规则
- 只有在
1 | package main |
1 | $ go build -gcflags='-m' escape.go |
较大的对象会被放在堆上- 如果对象分配在
栈上,其管理成本比较低,只需要挪动栈顶寄存器就可以实现对象的分配和释放 - 如果对象分配在
堆上,需要经过层层的内存申请过程
逃逸分析和垃圾回收结合,可以极大地降低开发者的心智负担,无需再担心内存的分配和释放
抽象成本
Rust -
零成本抽象
- 一切
抽象皆有成本,要么花在编译期,要么花在运行期 - GC 是选择在
运行期来解决问题,在极端场景下,GC 可能会占用大量的 CPU 资源
内存管理
主要角色
mutator
- 一般指
应用,即 Application - 可以将
堆上的对象看作一张图,应用被称为 mutator 的主要原因- 应用在不停地
修改堆对象的指向关系
- 应用在不停地
allocator
- 即
内存分配器,应用需要向 allocator 申请内存 - allocator 要维护
内存分配的数据结构,并考虑在高并发的场景下,降低锁冲突
garbage collector
- 即
垃圾回收器 死掉的堆对象和不用的堆内存都由 garbage collector 回收,并归还给OS- 过程简述
- 当 GC
扫描流程开始执行时,garbage collector 需要扫描内存中存活的堆对象 - 扫描完成后,未被扫描到的对象就是
无法访问的堆上垃圾,需要回收其占用内存
- 当 GC
交互过程
- 内存分配
- mutator 需要在
堆上申请内存时,会由编译器自动调用runtime.newobject - 此时 allocator 使用
mmap系统调用从OS申请内存 - 如果 allocator 发现之前申请的内存还有富余
- 会从本地
预先分配的数据结构中划分出一块内存,并以指针的形式返回给应用
- 会从本地
- 在内存分配的过程中,allocator 负责维护内存管理对应的数据结构
- mutator 需要在
- 垃圾回收
- garbage collector 扫描由 allocator 管理的数据结构
- 将应用不再使用的内存,通过
madvise系统调用归回给OS
内存分配
分配视角
mmap返回的是连续的内存空间,mutator以应用视角来申请内存,需要allocator进行映射转换
应用视角:需要初始化的 a 是一个 1024000 长度的切片内存管理视角:需要管理的只是 start + offset 的一段内存
分配效率
- 应用运行期间会不断地创建和销毁
小对象,如果每次对象的分配和释放都需要与OS交互,成本极高 - 应该在
应用层设计好内存分配的多级缓存,尽量减少小对象高频创建和销毁时的锁竞争- 如 C/C++ 的
tcmalloc,而 Go 的内存分配器基本1:1搬运了 tcmalloc
- 如 C/C++ 的
- tcmalloc 通过维护一套
多级缓存结构- 降低了应用内存分配过程中对
全局锁的使用频率,使得小对象的内存分配尽量做到无锁
- 降低了应用内存分配过程中对
在 Go 中,根据对象中是否有
指针以及对象的大小,将内存分配过程分为 3 类
| Type | Cond |
|---|---|
| tiny | size < 16 bytes && has no pointer(noscan) |
| small | has pointer(scan) || (size >= 16 bytes && size <= 32 KB) |
| large | size > 32 KB |
tiny
可以将内存分配的路径与 CPU 的多级缓存作类比,
mcache是本地的,而mheap为全局的L4是以页为单位将内存向下派发,由pageAlloc来管理arena中的空闲内存
如果L4依然无法满足内存分配需求,则需要向 OS 申请内存
| L1 | L2 | L3 | L4 |
|---|---|---|---|
| mcache.tiny | mcache.alloc[] | mheap.central | mheap.arenas |
small
与 tiny 的分配路径相比,缺少
mcache.tiny
| L1 | L2 | L3 |
|---|---|---|
| mcache.alloc[] | mheap.central | mheap.arenas |
large
- 直接从
mheap.arenas申请内存,直接走pageAlloc页分配器 pageAlloc页分配器在 Go 中迭代了多个版本,查找时间复杂度从O(N) -> O(log(n)) -> O(1)- 在
O(1)的时间复杂度便可确定能否满足内存分配需求 - 如果不满足,则需要对 arena 继续进行
切分,或者向OS申请更多的 arena
- 在
数据结构
arena是 Go 向 OS 申请内存的最小单位,每个 arena 的大小为64 MB,是一个部分连续但整体稀疏的内存结构- 单个 arena 会被切成以
8KB为单位的page,由page allocator管理 - 一个或者多个
page可以组成一个mspan,每个 mspan 可以按照sizeclass再划分成多个element - 同样大小的 mspan 又分为
scan和noscan两种,分别对应内部有指针的对象和内部没有指针的对象
每个
mspan都有一个allocBits结构
从 mspan 里面分配element 时,只需要将 mspan 对应的 element 位置的 bit 设为1即可
垃圾回收
回收算法
- Go 使用的 GC 算法:
并发标记清除- 将内存中
正在使用的对象进行标记,然后清除那些未被标记的对象
- 将内存中
- 并发:GC 的
标记和清扫过程,能够与应用代码并发执行,不要与程序设计的并发设计混淆 并发标记清除算法有个无法解决的缺陷:即内存碎片- 在 JVM 的
CMS,会有内存压缩整理的过程 - 而 Go 的内存管理是基于
tcmalloc,本身基于多级缓存,能在一定程度上缓解内存碎片的问题
- 在 JVM 的
垃圾分类
语义垃圾
semantic garbage,也称
内存泄露,从语法上可达的对象,但从语义上,这些对象为垃圾,GC无法回收语义垃圾
slice 缩容后,底层数组的后两个元素已经无法再访问了,但其关联的堆上内存依然
无法被释放
因此在 slice 缩容前,应该先将底层数组元素置为nil
语法垃圾
syntactic garbage,从
语法上已经不可达的对象,这才是 GC 的回收目标
1 | package main |
1 | $ go run -gcflags="-m" alloc_on_heap.go |
回收流程
关键流程
stw可以使用pprof的pauseNs来观测,也可以直接采集到监控系统,官方宣称达到亚毫秒级
标记
三色抽象
Go 使用
三色抽象,主要为了让 GC 和应用能并发执行
| Color | Desc |
|---|---|
| 黑 | 本节点已经扫描完毕,子节点扫描完毕(gcmarkbits = 1,且在队列外) |
| 灰 | 本节点已经扫描完毕,子节点尚未扫描完毕(gcmarkbits = 1,且在队列内) |
| 白 | 本节点未扫描,garbage collector 不知道任何相关信息 |
GC 扫描的起点是
根对象(从.bss、.data、goroutine的栈开始扫描,最后遍历整个堆上的对象树)
广度优先遍历
gc mark worker 会一边从 gcw 弹出对象,一边把其子对象压入 gcw,如果 gcw 满,压入全局队列
堆上对象的本质为图,在标记过程中,会有简单的剪枝逻辑,防止重复标记,浪费计算资源
1 | // If marked we have nothing to do. |
多个 gc mark worker 可能会产生竞态条件,通过
atomic.Or8来保证并发安全
1 | // setMarked sets the marked bit in the markbits, atomically. |
协助标记
- 当应用
分配内存过快时,后台的 gc mark worker无法及时完成标记工作 - 此时应用在进行
堆内存分配时,会判断是否需要协助 GC 的标记过程,防止应用OOM- 相当于让应用线程
让出部分算力给 GC 线程 - 但协助标记会对
应用的响应延迟产生影响
- 相当于让应用线程
- Go 内部通过一套
记账还账系统来实现协助标记流程
对象丢失
在
并发标记期间,应用还会不断地修改堆上对象的引用关系,可能会导致对象丢失问题
漏标 B,导致 B 被错误回收,如果堆上的对象引用关系满足
三色不变性,便能解决对象丢失问题
强三色不变性:禁止黑色对象指向白色对象
基于的假设:黑色已经处于终态了,GC 不会再关注
弱三色不变性:黑色对象可以指向白色对象,但指向的白色对象,必须有能从灰色对象可达的路径
基于的假设:虽然黑色已经处于终态了,但灰色还在处理中,还能补救
无论在
并发标记期间,应用如何修改对象的关系,只要能保证在修改后,堆上的对象能满足三色一致性即可
三色一致性的实现依赖于屏障技术,在 Go 中,即写屏障(Go 只有write barrier,没有 read barrier)
回收
进程启动时会有两个特殊的 goroutine
| goroutine | desc |
|---|---|
| sweep.g | 主要负责清扫死对象,合并相关的空闲页 |
| scvg.g | 主要负责向 OS 归还内存 |
- 当 GC 的标记流程结束后,sweep.g 会被唤醒,进行清扫工作
- 针对每个
mspan,sweep.g 将并发标记期间生成的 bitmap替换掉分配时使用的 bitmap
根据 mspan 中
槽位情况来决定 mspan 的未来
- 如果 mspan 中存活对象数为 0,即所有 element 都是垃圾
- 执行
freeSpan,归还组成该 mspan 所使用的 page,并更新全局的页分配器的摘要信息
- 执行
- 如果 mspan 中没有空槽,说明所有对象都是存活的,将其放入
fullSwept队列中 - 如果 mspan 中有空槽,说明此 mspan 还可以用来做内存分配,将其放入
partialSweep队列中
然后 scvg.g 被唤醒,将
页内存归还给OS




























