Golang内存模型与源码解析
0、引言
本篇笔记用于记录作者在学习Golang的GC模型之前,对Golang内存模型的学习。目前使用的Go版本为1.22.4
1、Golang内存管理宏观结构
假设我们每次向内存池申请空间时,都需要频繁地向操作系统发出请求,这不仅会增加内存分配的时间,还可能引入竞争和锁的开销,从而导致性能瓶颈。尤其是在多线程并发的开发场景下,这样的问题带来的损耗是显而易见的。为了减少这种开销,我们不妨在初始化内存空间时,一次性地向操作系统申请多一点的空间,只有当现有空间不足的时候,再次向操作系统申请新的空间。通过这种方式,可以有效减少内存分配过程中多线程并发带来的竞争,提高程序的性能。Go语言的内存管理模型正是围绕着高效的内存分配机制和垃圾回收机制来优化这类问题的,从而在大规模并发应用中取得更好的性能表现。
Golang的内存管理结构宏观图如下:
设计到的核心数据结构有:
mheap
:Golang内存模型中最大的内存池,是全局的内存起源,它直接和操作系统进行内存申请交互,向mheap申请内存需要持有锁。mcentral
:mheap的粒度细化的内存池,存在于mheap中,总数量为136(Span Class)个。mcache
:每个P持有的一份本地内存缓存,访问其不需要持有锁。
接下来我们来详细了解需要接触到的相关概念。
2、内存管理模型相关概念及源码解析
2.1、page
借鉴操作系统内存分页管理的思想,Golang的内存管理模型也存在着Page
,其是内存管理模型和操作系统内存交互的最小单元,大小为8KB,对于Golang来说,操作系统的虚拟内存就是被划分成N个Page的大内存池。
2.2、mspan
多个连续的page
被称之为mspan
,其大小为8KB~32KB。其根据分配object
大小来划分可以划分为67种。
其源码的核心字段如下:
type mspan struct { //标识前后mspan的指针 next *mspan prev *mspan //起始地址 startAddr uintptr //包含的页数 npages uintptr // freeindex 是一个槽索引,范围在 0 到 nelems 之间,表示开始扫描该 span 中下一个空闲对象的位置。 // 每次分配都会从 freeindex 开始扫描 allocBits,直到遇到一个 0,表示找到一个空闲对象。 // 随后,freeindex 会调整为刚发现的空闲对象之后的位置,以便下次扫描从新的位置开始。 // // 如果 freeindex == nelem,表示这个 span 中没有空闲对象。 freeindex uint16 //该span中的object的数量 nelems uint16 //是 allocBits 的部分缓存,且保存的是 allocBits 的补码。 allocCache uint64 //mspan的等级 spanclass spanClass // size class and noscan (uint8) }
next
与prev
用于指向同规格下的上一mspan与下一mspan,将整条mspan封装成链表,有助于扩展和销毁。startAddr
用于记录起始地址。nelems
用于记录当前mspan
中object的数量。freeindex
用于标识下一次扫描寻找object的位置,在该位置前的object都已经被使用。
2.3、object
object
是协程应用逻辑一次向Golang申请的对象。object
是golang
内存管理模型针对内存分配更加细化的内存管理单元,一个mspan
在初始化时会被划分为多个object
。例如一个大小为8B的object
归属于大小为8KB的mspan
,该mspan
被划分为1024个object。object根据大小可以从8B~32KB划分为67种。golang
内存管理内部本身用来给对象存储内存的基本单元是object
。
下图可以展示object、page、mspan三者间的关系。
2.4、SizeClass与SpanClass
SizeClass
是针对Object的大小来进行划分的等级,标识着每次申请空间的容量对应着哪一个等级。例如一次内存请求中,申请获得1B~7B之间的容量,那都归属于SizeClass 1
级别。
而SpanClass
是针对mspan
来划分的,指span
大小的级别。(虽然mspan
的大小只能为page
的整数倍,最高只能为32KB,但是因为一个mspan
可以被不同大小的object
划分,因此mspan
具有多个种类)。一个SizeClass
对应着两个SpanClass
,其中一个SpanClass
为存放需要GC扫描的对象,而另一个则存放不需要GC扫描的对象。
其对应关系图可以用下图来表示。
通过sizeclass
生成spanclass
的源码如下:
type spanClass uint8 // uint8 左 7 位为 mspan 等级,最右一位标识是否为 noscan func makeSpanClass(sizeclass uint8, noscan bool) spanClass { return spanClass(sizeclass<<1) | spanClass(bool2int(noscan)) } func (sc spanClass) sizeclass() int8 { return int8(sc >> 1) } func (sc spanClass) noscan() bool { return sc&1 != 0 }
生成规则为现将sizeclass
左移一位,即乘2,最小位标识是否为noscan。
2.5、mcache
mcache
被一个P持有,作为其本地缓存,当运行在和当前P绑定的线程上,需要申请内存资源时,会优先从mcache
上获得,因为一个P在同一时刻只能有一个M在其上运行,因此访问mcache
不需要持有锁,加快了内存分配。
mcache
在初始化时,持有每一种spanclass
的一个mspan
实体,不同spanclass
的Mspan
长度会不同
其源码的核心字段如下:
type mcache struct { //微对象分配器 tiny uintptr tinyoffset uintptr tinyAllocs uintptr //缓存的mspan,每一种spanclass有一个mspan alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass }
mcache
总的tiny
字段用于处理小于16B对象的内存分配,将会在下文提及。
2.6、mcentral
mcentral
作为中心缓存,起到分配小对象空间分配的作用,当mcache
中的mspan
空间不足的时候,就会尝试向mcentral
获取一份mspan
进行补充。有多少个spanclass
等级,就存在着多少个mcentral
,每一个mcentral
只负责自己等级的mspan
分配。
核心字段如下:
type mcentral struct { spanclass spanClass //维护全部空闲的span集合 partial [2]spanSet //维护存在非空闲的span集合 full [2]spanSet }
mcentral
持有两个mspan
集合,一个集合用于存放含有可用空间的mspan
即partial
集合,另一个则存放没有可用空间的mspan
即full
集合。每一个集合长度为2,是因为有一条用于处理GC。
2.7、mheap
对于golang
的上层应用而言,mheap
就是它们眼中的操作系统虚拟内存,通过向mheap
申请内存而不是每次都向操作系统申请开辟空间,可以减少其开销。mheap
的上游就是mcentral
,当mcentral
的内存不够时,就会以page
为单位向mheap
请求空间,而当mheap
的空间不够时,则会向下游的操作系统申请空间,申请的单位为64M
。
type mheap struct { // 堆的全局锁 lock mutex // 空闲页分配器 pages pageAlloc // 记录了所有的 mspan. 需要知道,所有 mspan 都是经由 mheap,使用连续空闲页组装生成的 allspans []*mspan // heapAreana 数组,64 位系统下,二维数组容量为 [1][2^22] // 每个 heapArena 大小 64M,因此理论上,Golang 堆上限为 2^22*64M = 256T arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena // ... // 多个 mcentral,总个数为 spanClass 的个数 central [numSpanClasses]struct { mcentral mcentral // 用于内存地址对齐 pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } // ... }
3、内存分配流程
不同的层次之间申请内存的单位如下图所示:
根据每次申请的内存的大小来划分,可以分为三种情况:
- 在(0,16B)之间,进入tiny对象分配流程
- 在[16B,32KB]之间,进入小对象分配流程
- 大于32KB,进入大对象分配流程
三种情况的具体流程都在mallocgc
函数中有具体的体现,其具体遵循以下的步骤:
- tiny对象分配:若申请内存大小为0B,则直接返回一个表示空字节的地址,该地址在程序初始化的时候就将确定不会发生改变;若申请内存大小为(0,16B],且不包含指针对象,则进入微对象分配流程,尝试从本地
mcache
缓存中的tiny
分配器中获取内存,若内存不足,则会进入到向mcentral
申请sizeclass
为2的mspan
的流程,将获取到的mspan
补充到mcache
,然后再重新获得tiny
内存;若mcentral
也不足,则会向mheap
申请page
补充到mcentral
,再进入之前的步骤。 - 小对象分配:与tiny对象分配类似,先根据
object
大小,找到对应的spanclass
级别,在mcache
查找该spanclass
下的span
是否还有容量,有则获取,否则向下游申请分配。 - 大对象分配:P将直接约过
mcache
和mcentral
,向mheap
获取指定的pages
。
3.1、源码一览
3.1.1、主流程mallocgc
mallocgc
函数定位于runtime/mheap.go
中,主体流程如下:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { //若请求的size为0,就返回一个固定的表示零空间的地址 if size == 0 { return unsafe.Pointer(&zerobase) } //... //获取m mp := acquirem() //标识正在进行内存获取,防止被gc抢占 mp.mallocing = 1 //... //获取mcache c := getMCache(mp) var span *mspan var header **_type var x unsafe.Pointer //根据当前对象是否包含指针,标识gc时是否需要展开扫描 noscan := typ == nil || typ.PtrBytes == 0 //是否是<=32KB的小对象、微对象 if size <= maxSmallSize-mallocHeaderSize { //小于16B并且没有指针,进入微对象分配 if noscan && size < maxTinySize { //tiny内存块中,从off开始存在空闲空间 off := c.tinyoffset //... //当前tiny块内存够用,则进行直接分配并且返回 if off+size <= maxTinySize && c.tiny != 0 { //分配内存 x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.tinyAllocs++ mp.mallocing = 0 releasem(mp) return x } //tiny空间不够,需要先申请。 //tinyspanclass为5 span = c.alloc[tinySpanClass] //尝试从mcache获取大小为16B的内存块,为0表示获取失败 v := nextFreeFast(span) if v == 0 { //从mcentral、mheap获取兜底 v, span, shouldhelpgc = c.nextFree(tinySpanClass) } //分配空间 x = unsafe.Pointer(v) (*[2]uint64)(x)[0] = 0 (*[2]uint64)(x)[1] = 0 if !raceenabled && (size < c.tinyoffset || c.tiny == 0) { c.tiny = uintptr(x) c.tinyoffset = size } size = maxTinySize } else { hasHeader := !noscan && !heapBitsInSpan(size) if goexperiment.AllocHeaders && hasHeader { size += mallocHeaderSize } //根据对象大小,映射其所属的span等级 var sizeclass uint8 if size <= smallSizeMax-8 { sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)] } else { sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)] } //分配给object空间的大小,0~32KB size = uintptr(class_to_size[sizeclass]) // 创建 spanClass 标识,其中前 7 位对应为 span 的等级(0~66),最后标识表示了这个对象 gc 时是否需要扫描 spc := makeSpanClass(sizeclass, noscan) //获取mcache的span span = c.alloc[spc] //尝试从该span中获取空间 v := nextFreeFast(span) if v == 0 { //获取失败,尝试从mcentral、mheap获取 v, span, shouldhelpgc = c.nextFree(spc) } x = unsafe.Pointer(v) //... } //大于32KB的大对象,直接尝试从mheap获取 } else { //从mheap获取 span = c.allocLarge(size, noscan) span.freeindex = 1 span.allocCount = 1 size = span.elemsize x = unsafe.Pointer(span.base()) /... } return x }
3.1.2、nextFreeFast
nextFreeFast
用于快速从mspan
中获取object
// nextFreeFast returns the next free object if one is quickly available. // Otherwise it returns 0. func nextFreeFast(s *mspan) gclinkptr { //寻找首个object空位,没有空闲对象则返回64 theBit := sys.TrailingZeros64(s.allocCache) // Is there a free object in the allocCache? if theBit < 64 { result := s.freeindex + uint16(theBit) //确保该索引未超过该span的对象范围 if result < s.nelems { //freeidx是新的freeindex freeidx := result + 1 //超出了nelemes数量,则返回0 if freeidx%64 == 0 && freeidx != s.nelems { return 0 } s.allocCache >>= uint(theBit + 1) s.freeindex = freeidx s.allocCount++ //返回获取object空位的内存地址 return gclinkptr(uintptr(result)*s.elemsize + s.base()) } } return 0 }
3.1.3、nextFree
nextFree
函数会首先尝试向mspan
获取对应大小的object
,若获取失败,则会向下游请求补充内存。
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) { s = c.alloc[spc] // ... // 从 mcache 的 span 中获取 object 空位的偏移量 freeIndex := s.nextFreeIndex() if freeIndex == s.nelems { // ... // 倘若 mcache 中 span 已经没有空位,则调用 refill 方法从 mcentral 或者 mheap 中获取新的 span c.refill(spc) // ... // 再次从替换后的 span 中获取 object 空位的偏移量 s = c.alloc[spc] freeIndex = s.nextFreeIndex() } // ... v = gclinkptr(freeIndex*s.elemsize + s.base()) s.allocCount++ // ... return }
//为mcache获取一个spanclass级别的mspan,这个mspan会至少含有一个空的object。当前mcache中的span必须满了,才会调用此方法。 func (c *mcache) refill(spc spanClass) { s := c.alloc[spc] // ... // 从 mcentral 当中获取对应等级的 span s = mheap_.central[spc].mcentral.cacheSpan() // ... // 将新的 span 添加到 mcahe 当中 c.alloc[spc] = s }
//从mcentral申请一个span,将被用在mcahce中。 func (c *mcentral) cacheSpan() *mspan { // ... var sl sweepLocker // ... //尝试清扫和分配未清扫的mspan sl = sweep.active.begin() if sl.valid { for ; spanBudget >= 0; spanBudget-- { s = c.partialUnswept(sg).pop() // ... if s, ok := sl.tryAcquire(s); ok { // ... sweep.active.end(sl) goto havespan } // 通过 sweepLock,加锁尝试从 mcentral 的非空链表 full 中获取 mspan for ; spanBudget >= 0; spanBudget-- { s = c.fullUnswept(sg).pop() // ... if s, ok := sl.tryAcquire(s); ok { // ... sweep.active.end(sl) goto havespan } // ... } } // ... } // ... // We failed to get a span from the mcentral so get one from mheap. s = c.grow() if s == nil { return nil } // 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了 havespan: // ... return }
3.1.4、mcentral.grow
mcentral.grow
方法用于mcentral
向mheap
申请分配一个新的mspan
。
// Grow从堆中分配一个新的空span,并为c的size类初始化它。 func (c *mcentral) grow() *mspan { //确定需要的页数和大小 npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) size := uintptr(class_to_size[c.spanclass.sizeclass()]) //从堆中分配一个新的mspan s := mheap_.alloc(npages, c.spanclass) //分配失败则返回nil if s == nil { return nil } //确定当前mspan可以容纳多少个对象 n := s.divideByElemSize(npages << _PageShift) s.limit = s.base() + size*n s.initHeapBits(false) //分配成功 return s }
// alloc 从 GC 管理的堆中分配一个新的 npage 页的span。 // // spanclass 指示span的大小类别和可扫描性。 // // 返回一个已完全初始化的span。span.needzero 表示 // 该span是否已被置零。请注意,它可能并未被置零。 func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan { var s *mspan systemstack(func() { if !isSweepDone() { h.reclaim(npages) } //转入allocspan方法 s = h.allocSpan(npages, spanAllocHeap, spanclass) }) return s }
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) { //初始化 gp := getg() base, scav := uintptr(0), uintptr(0) growth := uintptr(0) needPhysPageAlign := physPageAlignedStacks && typ == spanAllocStack && pageSize < physPageSize //如果不需要物理页对齐且页数小于缓存容量的四分之一,尝试使用 P 的局部页缓存。 pp := gp.m.p.ptr() if !needPhysPageAlign && pp != nil && npages < pageCachePages/4 { c := &pp.pcache // If the cache is empty, refill it. if c.empty() { lock(&h.lock) *c = h.pages.allocToCache() unlock(&h.lock) } base, scav = c.alloc(npages) if base != 0 { s = h.tryAllocMSpan() if s != nil { goto HaveSpan } } } //局部缓存分配失败,则锁住全局堆进行分配 lock(&h.lock) //.... //从全局堆分配 if base == 0 { // Try to acquire a base address. base, scav = h.pages.alloc(npages) //如果堆空间不足,则触发堆增长 if base == 0 { var ok bool growth, ok = h.grow(npages) if !ok { unlock(&h.lock) return nil } base, scav = h.pages.alloc(npages) if base == 0 { throw("grew heap, but no adequate free space found") } } } if s == nil { // We failed to get an mspan earlier, so grab // one now that we have the heap lock. s = h.allocMSpanLocked() } unlock(&h.lock) //... HaveSpan: // 把空闲页组装成 mspan s.init(base, npages) // 将这批页添加到 heapArena 中,建立由页指向 mspan 的映射 h.setSpans(s.base(), npages, s) // ... return s }
3.1.5、mheap.grow
mheap.grow
用于mheap
向操作系统获取虚拟内存。
func (h *mheap) grow(npage uintptr) (uintptr, bool) { av, asize := h.sysAlloc(ask) }
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { v = sysReserve(unsafe.Pointer(p), n) }
func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer { return sysReserveOS(v, n) }
func sysReserveOS(v unsafe.Pointer, n uintptr) unsafe.Pointer { //通过 mmap 向操作系统请求一块虚拟内存。 p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0) if err != 0 { return nil } return p }
4、参考博客
[[Go三关-典藏版]一站式Golang内存管理洗髓经 - 知乎](https://zhuanlan.zhihu.com/p/572059278#:~:text=本文收录于 《Golang修养之路》Golang的内存管理及设计也是开发者需要了解的领域之一,要理解)
本文作者:MelonTe
本文链接:https://www.cnblogs.com/MelonTe/p/18618032
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步