GO的内存管理
这其实和上一篇文章“GMP调度模型”有着一定关联,当一个协程通过GMP模型调度产生对象以后,就要涉及到Go的内存管理机制了。本文的内存管理是一个比较大的概念,包含了垃圾分配和对象内存管理。
0 整体架构&概念
首先要明确自动内存管理其实就是指垃圾回收,由程序语言的运行时系统(runtime)管理动态内存。另外,几个英文对照的概念:
- Mutator:业务线程,分配新对象,修改对象指向关系
- Collector:GC线程,找到存活对象,回收死亡对象的内存空间
- allocator:内存分配器,应用需要内存都要向allocator申请
- Serial GC:只有一个collector
- Parallel GC:支持多个collectors同时回收的GC算法
- Concurrent GC:mutator(s)和collector(s)可以同时执行
1 Go垃圾回收
首先,垃圾回收主要可以从以下方面讨论:
- 垃圾分类
- 标记流程
- 清理过程
1.1 垃圾分类
语法垃圾和语义垃圾
语义垃圾
这是垃圾回收器无法回收的垃圾,通常是由于程序员的不正当操作导致的内存泄露而产生的。
如图,数组后面的两个元素无法再访问了,但其关联的堆上内存依然是无法释放的。
语法垃圾
也就是指针指向关系不可达的对象,是垃圾回收的对象
1.2 标记流程
众所周知,Go使用的标记算法是三色标记法。那为什么不适用引用计数法呢?由于其在并发时不可扩展,对于Go这样的高并发语言并不适合。
在以下文章总结了Go不同版本的标记算法,图文精炼,故我就不写了= =,真不是懒QAQ
[典藏版]Golang三色标记、混合写屏障GC模式图文全分析 - SegmentFault 思否
1.3 清理过程
相对于标记流程,对象的清理和内存释放就简单多了
进程启动时会有两个特殊 goroutine:
- 一个叫
sweep.g
,主要负责清扫死对象,合并相关的空闲页 - 一个叫
scvg.g
,主要负责向操作系统归还内存
当GC的标记流程结束,就会启动sweep.g
,进行清扫工作。之后scvg.g
被唤醒,执行线性流程,将页内存归还给操作系统。
为什么Go不使用分代GC?
由于分代假说假定大部分变为垃圾的对象都是新创建的对象,但是在Go语言中由于编译器的优化,通过内存逃逸的机制,将会继续使用的对象转移到了堆中。大部分新创建的对象很快变为垃圾的对象会在栈中分配。这和其他使用隔代GC的编程语言有显著的不同,这减弱了使用隔代GC的优势。同时, 隔代GC需要额外的写屏障来保护并发垃圾回收时对象的隔代性,这会减慢GC的速度。因此,隔代GC是被尝试过并抛弃的方案
2 Go内存分配
2.1 分块
目标:为对象在heap上分配内存
将内存分块:
- 调用系统调用
mmap()
,向OS申请一大块内存,例如4MB - 先将内存划分成大块,例如8KB,称为mspan
- 再将大块继续划分为特定大小的小块,用于对象分配
noscan mspan
:分配不包含指针的对象——GC不需要扫描scan mspan
:分配包含指针的对象——GC需要扫描
对象分配:根据对象的大小,选择最合适的块返回
2.2 缓存
使用了TCMalloc18张图解密新时代内存分配器TCMalloc (qq.com)
- 每个p包含一个
mcache
用于快速分配,用于为绑定于p上的g分配对象 mcache
管理一组mspan
- 当
mcache
的mspan
分配完毕,向mcentral
申请带有未分配块的mspan
- 当
mspan
中没有分配的对象,mspan会被缓存在mcentral
中,而不是立刻释放并归还给OS
2.3 字节的优化方案
每个g都绑定一大块内存(1KB),称为goroutine allocation buffer
(GAB)
GAB用于noscan
类型的小对象分配:<128B
使用三个指针维护GAB:base,end,top
Bump pointer(指针碰撞)风格对象分配
- 无须和其他分配请求互斥
- 分配动作简单高效
如图,只需要移动指针就可以完成对象的分配
存在的问题
- GAB对于Go内存管理是一个大对象
- 本质:将多个小对象的分配合并成一个大对象的分配
- 问题:GAB的对象分配方式会导致内存被延迟释放
- 方案:移动GAB中存活的对象
- 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配GAB中
- 原先的GAB可以释放,避免内存泄露
- 本质:用copying GC的算法管理小对象
3 总结
Go的内存管理包括了垃圾回收和内存分配,之前有看过Java这方面的知识,感觉还是挺容易理解的,并且Go没有垃圾回收器,TLAB等概念,感觉八股这块还是略弱于Java哈哈。