1、前言
现在的高级语言为了降低开发难度,都会将内存管理纳入语言特性中,包括内存的申请、回收、释放等极易引起血案的操作。内存操作在程序运行过程中是极其频繁的,而向系统申请、释放内存涉及到系统调用,频繁的系统调用很容易成为性能瓶颈,因此良好的内存管理架构很大程度影响着服务的性能。Go作为新生代热门语言,它的内存管理是如何做的,本文接下来会从源码层面进行详细的介绍。
2、内存结构
Go内存管理借鉴了Google开发的内存管理器tcmalloc的架构。整体架构如下:
内存管理主要分为:系统内存、全局heap内存、每个P管理的局部内存cache,内存的管理单元是按照span组织,每个span管理多个连续page,每个page大小8KB。
运行时内存申请优先从P的局部内存池中获取,该方式无需加锁,当局部内存不满足时,则从全局heap内存池中获取,该方式需要加锁。
2.1 全局heap内存管理
mheap定义如下:
type mheap struct { lock mutex //全局操作锁 pages pageAlloc allspans []*mspan //所有创建的mspan列表 //表示可用的虚拟地址空间,其管理的空间已经调用sysReserve处于Reserved状态,具体状态含义见第7节。 arenas [1 << arenaL1Bits][1 << arenaL2Bits]*heapArena arenaHints *arenaHint //用于分配heap arena的提示地址 //当前使用的arena,其中的虚拟地址空间已经调用sysMap处于Prepared状态,具体状态含义见第7节。 curArena struct { base, end uintptr } //136长度的central数组,用于管理mspan,每个central管理不同对象大小的一系列mspan central [numSpanClasses]struct { mcentral mcentral pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } spanalloc fixalloc //mspan分配器 cachealloc fixalloc //mcache分配器 arenaHintAlloc fixalloc //arenaHints分配器 ... }
mheap是所有协程共用的全局内存管理结构,该结构通过arenas二维数组表示可用的虚拟空间,初始时并未实际分配,在运行过程中通过系统调用映射实际的内存。arenas的第一维长度、第二维长度分别为L1 entries、L2 entries,该长度根据操作系统类型变化,详细如下:
(1 << addr bits) = arena size * L1 entries * L2 entries
注:L2 entries每个元素是一个指针,64-bit时是8字节,32-bit时是4字节,因此arenas占用空间需要乘以字节数。
mheap中central数组用于管理mspan,主要用于小对象分配(32KB),每个central管理一系列mspan,同一个central的mspan具有相同对象大小,不同central的mspan对象大小可能不同,因为相同对象大小的mspan还分为指针对象和非指针对象两种。默认对象大小共68种,加上指针对象和非指针对象两类,共136种mspan(其中前两个对象大小为0)。默认对象大小列表见文件runtime/sizeclasses.go中class_to_size定义。
2.2 局部P内存管理
p定义如下:
type p struct { mcache *mcache //管理mspan,用于快速对象分配 pcache pageCache //缓存来自mheap的page,用于快速page分配 ... }
P管理的局部内存用于G运行时的无锁快速分配。其中mcache管理一个136长度的数组,用于小对象快速分配,每个数组元素为一个mspan指针,和central中mspan类似,每个mspan的对象大小不一定相同。
pcache缓存来自mheap的page,用于运行时快速分配多个page。
2.3 span内存结构
mspan定义如下:
type mspan struct { next *mspan //构成链表时,后一个mspan,不存在时为nil prev *mspan //构成链表时,前一个mspan,不存在时为nil startAddr uintptr //mspan管理空间的首地址 npages uintptr // mspan管理空间的page数 freeindex uintptr //查找下一个空闲对象的起始点 allocCache uint64 //标记freeindex后64个对象空闲状态,用于快速对象分配 nelems uintptr //mspan能存储的对象个数 elemsize uintptr //对象大小 allocCount uint16 //已分配的对象个数 ... }
mspan是使用多个连续的page管理指定大小对象的结构,该空间的首地址是startAddr,freeindex是查找下一个空闲对象的起始点,为了加快查找,使用allocCache缓存freeindex后64个对象的空闲状态。
3、内存分配
当你在代码中new一个对象时,编译器会将new改写为newobject调用,newobject即为内存分配的入口:
3.1 分配流程
内存分配调用链:
函数说明:
- newobject:创建对象入口,编译器会将用户代码中的new转换为newobject调用。
- mallocgc:分配空间,分三种情况:1、超过32KB空间使用allocLarge直接向mheap申请;2、小于16B并且不包含指针的对象通过tiny allocator分配空间,该分配器可以将多个小对象共用一个16B的块空间,16B的块空间来自P的mcache.alloc[5];3、其他大小申请使用P的mcache.alloc对应槽。
- allocLarge:直接向mheap申请大空间。
- nextFreeFast:通过mspan上的allocCache快速获取局部空闲空间。因为allocCache为8字节,共64bit,每bit标识一个对象空间,因此只能管理64个对象空间的空闲状态。
- nextFree:获取mspan中空闲空间,如果没有空闲空间,则依次从mcentral、mheap逐级向上申请。
- nextFreeIndex:查找mspan全局空闲空间。
- refill:将满的mspan返回mcentral,并获取一个新的mspan。
- uncacheSpan:将指定mspan放入mcentral。
- cacheSpan:从mcentral获取一个mspan。
- push:将mspan插入队列。
- pop:从队列中获取mspan。
- grow:从mheap获取一个mspan。
- alloc:mheap从堆上分配一个mspan,该函数会调用mheap.allocSpan。
- allocSpan:先尝试从P.pcache中分配一个mspan,然后mheap.arena获取空间。
- grow:扩展mheap的arena空间。
- sysAlloc:调用各操作系统的内存分配函数。
- allocMSpanLocked:分配一个mspan,mheap必须已加锁。
3.2 分配实现
下面会详细介绍流程中各函数的具体实现。
3.2.1 mallocgc
mallocgc执行流程:
mallocgc分配内存时根据申请大小执行不同分配策略。当size>32KB时,直接调用mcache.allocLarge从mheap上分配空间。当size<16B并且不包含指针时,通过tiny allocator从mcache.alloc的16字节非指针槽中分配空间。其他情况从mcache.alloc的对应大小的槽中分配空间。
3.2.2 allocLarge
allocLarge用于直接在heap上分配大对象空间,执行流程:
3.2.3 tiny allocator
tiny allocator是针对小对象分配做的优化,使用了mcache中16字节非指针mspan,可以将多个对象分配到一个16字节中,执行流程:
3.2.4 nextFreeFast
nextFreeFast通过mspan中allocCache快速搜索空闲空间,allocCache使用64个bit判断小范围空间,bit为1表示空闲,执行流程:
3.2.5 nextFree
mcache.nextFree通过搜索allocCache表示范围之外的空闲空间,按照每8字节进行判断,找到空闲空间后更新allocCache,如果mspan没有空闲空间,则调用mcache.refill从mheap的对应大小central中获取mspan。执行流程:
mspan.nextFreeIndex使用步进8字节,逐步向后移动mspan.allocCache来判断下一个空闲位置,直到找到第一个空闲位置返回。
3.2.6 refill
mcache.refill先调用mheap.mcentral.uncacheSpan将满的mspan返回给mheap.mcentral,然后调用mheap.mcentral.cacheSpan获取一个新的mspan。执行流程:
3.2.7 uncacheSpan
mcentral.uncacheSpan执行流程:
3.2.8 cacheSpan
mcentral.cacheSpan执行流程:
3.2.9 spanSet.push
spanSet.push执行流程:
spanSet.index是8字节,高4字节表示head,用于获取mspan,低4字节表示tail,用于插入mspan。
spanSet.push用于将给定的mspan插入尾部,首先从index中获取尾部位置tail,tail除512可以得到spanSetBlock索引位置,tail模512可以得到mspan槽位置。当spineCap不足时,调用persistentalloc扩充spanSetBlock数组容量,并创建新spanSetBlock挂载。
3.2.10 spanSet.pop
spanSet.pop执行流程:
spanSet.pop用于从缓存队列头部获取一个mspan,如果某个spanSetBlock pop次数达到512说明该block为空,则释放该block。
3.2.11 allocSpan
mheap.allocSpan首先会从当前P的页缓存pcache中申请空间,如果失败会尝试调用mheap.pages申请空间,该函数会先从mheap.curArena中分配,如果失败会从系统申请。执行流程:
4、内存回收
内存回收发生在GC阶段,主要用于检测mcentral中未使用的mspan,将其回收到heap的pageAlloc中。
内存回收调用链:
函数说明:
- forcegchelper:sysmon唤醒,执行GC。
- gcenable:启动内存回收、释放协程。
- gcStart:开始执行GC。
- bgsweep:执行回收任务。
- sweepone:从central对应的full unswept或partial unswept列表中获取mspan进行回收工作。
- sweep:执行mspan的回收工作。
- freeSpan:将mspan回收到mheap。
- freeSpanLocked:将mspan管理的页空间归还给mheap.pages,并回收mspan对象本身。
- pages.free:将mspan管理的页空间归还给mheap.pages。
- freeMSpanLocked:将mspan对象归还给mheap.spanalloc。
5、内存释放
内存是否发生在GC阶段,主要用于将mheap中pageAlloc管理的部分空闲页归还给系统。
内存释放调用链:
函数说明:
- bgscavenge:后台内存释放任务。
- scavenge:释放指定大小的空间,循环调用scavengeOne,返回实际释放的内存量。
- scavengeOne:在指定地址范围内释放指定大小的空间。
- scavengeRangeLocked:释放指定区域内存。
- sysUnused:调用各操作系统的内存释放函数。
6、内存分配器
内存管理定义了三种内存分配器:fixalloc、linearAlloc、pageAlloc。fixalloc用于分配固定大小对象,linearAlloc用于一块内存的线性分配,pageAlloc用于分配page。
6.1 fixalloc
fixalloc用于分配固定大小对象,结构定义如下:
type fixalloc struct { size uintptr //每次分配对象的大小 list *mlink //复用对象列表 chunk uintptr //空闲块地址 nchunk uint32 //空闲块大小 inuse uintptr //已使用字节数 ... }
结构图如下:
fixalloc分配内存流程:
6.2 linearAlloc
linearAlloc是线性分配器,结构定义如下:
type linearAlloc struct { next uintptr //空闲空间起始地址 mapped uintptr //预留空间中已映射的空间地址 end uintptr //预留空间结束地址 }
结构图如下:
6.3 pageAlloc
pageAlloc是页分配器,结构定义如下:
type pageAlloc struct { searchAddr offAddr //下次分配空间时的搜索地址 start, end chunkIdx //管理空间的起始、结束地址,转换为chunk的索引 inUse addrRanges //正在使用的空间范围 chunks [1 << pallocChunksL1Bits]*[1 << pallocChunksL2Bits]pallocData //使用bit表示chunk中page的空闲状态 ... }
结构图如下:
- searchAddr:下次分配空间时的搜索地址,该地址必须在inUse范围中。
- start:pageAlloc管理空间的起始地址,转换为chunk的索引,每个chunk 4M。
- end:pageAlloc管理空间的结束地址,转换为chunk的索引,每个chunk 4M。
- inUse:正在使用的空间。
- chunks:二维数组,一个数组元素pallocData管理一个chunk中每个page的空闲状态,一个pallocData中包含8个uint64,共64字节,即512 bit,每个bit表示一个page的空闲状态,1表示空闲,0表示已分配,每个page 8K,刚好是一个chunk大小:512*8K=4M。
7、内存管理抽象层
为了管理不同操作系统的内存操作差异,增加了OS内存管理抽象层,该层封装了各操作系统的内存调用,并设定了内存空间的四种状态:None、Reserved、Prepared、Ready。状态说明如下:
- None:未预留且未映射,默认状态。
- Reserved:runtime拥有的空间,访问会报错。
- Prepared:预留的空间,但不对应物理内存,访问行为未定义,报错或者返回零(根据不同操作系统表现不同)。
- Ready:可安全访问。
各状态之间的转换关系如下:
状态转换函数说明如下:
- sysAlloc:转换内存状态从None到Ready,该函数从操作系统获取大块空间然后立即使用。
- sysFree:转换内存从任何其他状态到None。
- sysReserve:转换内存状态从None到Reserved。
- sysMap:转换内存状态从Reserved到Prepared,确保内存状态能有效的转换到Ready。
- sysUsed:转换内存状态从Prepared到Ready,该函数确保内存内存可以安全访问。
- sysUnused:转换内存状态从Ready到Prepared,该函数通知OS该内存对应的物理页不再使用。
- sysFault:转换内存空间状态从Ready或Prepared到Reserved,仅在debug时使用。
文章评论