本文已参与「新人创作礼」活动,一起开启掘金创作之路。
这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记
背景
自动内存管理和Go的内存管理机制
内存分配
GO中内存布局如下
mheap管理着地址空间的一大段连续的内存,以8K为一页,多个页组成一个个span,多个span组成一个arena。每个span只存储一种大小的元素
span的数据结构是mspan,span存储大小规格覆盖了小于等于32k的66种大小,对应的编号为1-66,记录再mspan的spanclass中。而大于32k的记录编号为0,会直接在mheap中分配。
type mspan struct {
next *mspan
prev *mspan
...
}
构成双向链表,使用runtime.mSpanList存储头结点和尾结点在mcache以及mcentral中使用
除此之外,还会按照元素是否含有指针来分类,存在spanclass中
- 存在指针 -- scan
- 不存在指针 -- no-scan 因此一共有134种
类型元数据的 _type的ptrdata就是用于区分需要放在scan还是no-scan中,ptrdata=0的会放在no-scan中,ptrdata不为0的会分配到scan中,会被gc扫描。
- 减少了gc耗时
go实现了全局和本地span缓存,防止每次进行堆空间分配的时候都要去申请一次
mheap.central记录了span的全局缓存,每一个mcentral代表了一种spanclass。并且将有空闲空间empty,没空闲空间nonempty分别管理
每个P都有一个mcache用于本地缓存,一样用spanclass区分,共134个mspan链表
- 微对象
(0, 16B)— 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存; - 小对象
[16B, 32KB]— 依次尝试使用线程缓存、中心缓存和堆分配内存; - 大对象
(32KB, +∞)— 直接在堆上分配内存;
每个处理器(P)都会分配一个线程缓存mcache用于处理微对象和小对象的分配,题目会持有不同规格的内存管理单元mspan。
每个类型的内存管理单元都会管理特定大小的对象,(8B,16B,32B...32KB)当内存管理单元中不存在空闲对象时,它们会从 mcentral 中获取新的内存单元,中心缓存属于全局的堆结构体 mheap,mheap会从操作系统中申请内存。
GC
目前go的gc使用的是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户程序并发执行)的三色标记清扫算法
- 对象整理的优势是解决了内存碎片化问题以及“允许”使用顺序内存分配器
-
- go运行时的分配算法基于tcmalloc,基本没有碎片问题
- 顺序内存分配器在多线程场景下不适用
- 分代假设没有带来直接的优势,且go的垃圾回收器与用户程序并发执行,使得STW的时间与对象的代际、对象的size没有关系
-
- go编译器会通过逃逸分析将大部分新生对象存储在栈上,会直接回收。当goroutine死亡后栈也会被直接回收,不需要gc的参与
具体流程如下
- GC在准备阶段会为每个P创建一个mark worker协程,即p.gcBgMarkWorker(现在好像变成了一个池),后台makr worker很快就进入休眠,等待到标记阶段进入执行
// Pool of GC parked background workers. Entries are type
// *gcBgMarkWorkerNode.
gcBgMarkWorkerPool lfstack
- 当第一次进入STW的时候,用于标记gc阶段的全局变量gcphrase= _GCMark,全局变量writeBarrier.enabled=true用于标记是否开启写屏障,全局变量gcBlackenEnabled=1用于标记是否允许进行gc mark工作
- 当STW结束后,所有p知道写屏障已经开启,然后后台的mark worker协程开始调度开始gc mark工作
- 当没有标记工作的时候,进入第二个STW,将gcphrase设置为 _GCMarkTermination,确认标记工作已经完成,并将gcBlackenEnabled标记为0
- 接下来进入 _GCOff阶段,将gcphrase标记为 _GCOff,关闭写屏障,设置writeBarrier.enabled=false,Start The World,进入清扫流程,bgsweep开始调度
-
- 在进入_GCOff之前新分配的对象为黑色,进入之后就是白色的
- 执行清扫工作的协程由runtime.main在gcenable中创建,标记为bgsweep。对应的g指针存储在全局变量sweep中
- 由于清扫工作也是增量进行的,因此在开始清扫前还要先保证完成上一次gc未完成的清扫工作
编译器优化基本问题和思路
- Go中由于语言特性,例如interface,defer等,限制了函数内联
- 内联策略非常保守
优化
内存管理优化 -- Balanced GC
为每个g都绑定一大块内存(1KB,也是相当于多个span的集合),称作goroutine allocation buffer(GAB)
- GAB用于对no-scan类型的小对象分配(这里指<128B的)
- 使用了三个指针维护GAB:base(指向开始),end(结束点),top(当前指针指向的地址,即用到了的地方)
- Bump pointer(指针碰撞风格对象的分配)
-
- 无须和其他分配请求互斥
-
- 分配动作简单高效
分析
本质是对多个小对象的分配合并成了一次大对象的分配,在每一次分配适当大小的no-scan小对象时,不需要走冗长的内存分配路径,直接从g中分配即可
带来的问题是GAB的对象分配方式会导致内存被延迟释放
GAB不是存储在g中,对go内存管理来说就是一个对象,可以直接交由go自己的内存管理机制管理
编译器优化 -- Beast Mode
调整函数内联的策略,使得更多函数被内联
- 降低函数调用的开销
- 增加了其他的机会:逃逸分析
产生的主要开销为
- Go镜像增加 约10%大小
- 编译时间增加