系统相关
首页 > 系统相关> > GO内存管理:内存组件之mspan、mcache、mcentral 和 mheap 数据结构

GO内存管理:内存组件之mspan、mcache、mcentral 和 mheap 数据结构

作者:互联网

原文:https://blog.haohtml.com/archives/29385

Golang中的内存组件关系如下图所示

components of memory allocationgolang 内存分配组件

在学习golang 内存时,经常会涉及几个重要的数据结构,如果不熟悉它们的情况下,理解起来就显得格外的吃力,所以本篇主要对相关的几个内存组件做下数据结构的介绍。

在 Golang 中,mcachemspanmcentral 和 mheap 是内存管理的四大组件,mcache 管理线程在本地缓存的 mspan,而 mcentral 管理着全局的 mspan 为所有 mcache 提供所有线程。

根据分配对象的大小,内部会使用不同的内存分配机制,详细参考函数 mallocgo()

对于golang中的内存申请流程,大家应该都非常熟悉了,这里不再进行详细描述。

Golang 内存组件关系

mcache

在GPM关系中,会在每个 P 下都有一个 mcache 字段,用来表示内存信息。

在 Go 1.2 版本以前调度器使用的是 GM 模型,将 mcache 放在了 M 里,但发现存在诸多问题,其中对于内存这一块存在着巨大的浪费。每个 M 都持有 mcache 和 stack alloc,但只有在 M 运行 Go 代码时才需要使用内存(每个 mcache 可以高达2mb),当 M 在处于 syscall 或 网络请求 的时候是不需要内存的,再加上 M 又是允许创建多个的,这就造成了内存的很大浪费。所以从go 1.3版本开始使用了GPM模型,这样在高并发状态下,每个G只有在运行的时候才会使用到内存,而每个 G 会绑定一个P,所以它们在运行只占用一份 mcache,对于 mcache 的数量就是P 的数量,同时并发访问时也不会产生锁。

对于 GM 模型除了上面提供到内存浪费的问题,还有其它问题,如单一全局锁sched.Lock、goroutine 传递问题和内存局部性等。

在 P 中,一个 mcache 除了可以用来缓存小对象外,还包含一些本地分配统计信息。由于在每个P下面都存在一个mcache ,所以多个 goroutine 并发请求内存时是无锁的。

mcache

当申请一个 16b 大小的内存时,会优先从运行当前G所在的 P 里的 mcache 字段里找到相匹配的 mspan 规格,是不需要锁的,此时最合适的是图中 mspan3 规格。

mcache是从非GC内存中分配的,所以任何一个堆指针都必须经过特殊处理。源码文件:https://github.com/golang/go/blob/go1.16.2/src/runtime/mcache.go

type mcache struct { // 下方成员会在每次访问malloc时都会被访问,所以为了更加高效缓存将其按组放在这里 nextSample uintptr // trigger heap sample after allocating this many bytes scanAlloc uintptr // bytes of scannable heap allocated   // 小对象缓存,<16b。推荐阅读"Tiny allocator"注释文档 tiny uintptr tinyoffset uintptr tinyAllocs uintptr   // 下方成员不会在每次 malloc 时被访问 alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass   stackcache [_NumStackOrders]stackfreelist   flushGen uint32 }

mcache.tiny 是一个指针,当申请对象大小为 <16KB 的时候,会使用 Tiny allocator 分配器,会根据tinytinyoffset 和 tinyAllocs 这三个字段的情况进行申请。

span 大小规格数据共有 67 类。源码里定义的虽然是 _NumSizeClasses = 68 类,但其中包含一个大小为 0 的规格,此规格表示大对象,即 >32KB,此种对象只会分配到heap上,所以不可能出现在 mcache.alloc 中。

mcache.alloc 是一个数组,值为 *spans 类型,它是 go 中管理内存的基本单元。对于16-32 kb大小的内存都会使用这个数组里的的 spans 中分配。每个span存在两次,一个不包含指针的对象列表和另一个包含指针的对象列表。这种区别将使垃圾收集的工作更容易,因为它不必扫描不包含任何指针的范围。

mspan

mspan 是分配内存时的基本单元。当分配内存时,会在mcache中查找适合规格的可用 mspan,此时不需要加锁,因此分配效率极高。

Go将内存块分为大小不同的 67 种,然后再把这 67 种大内存块,逐个分为小块(可以近似理解为大小不同的相当于page)称之为span(连续的page),在go语言中就是上文提及的mspan

6328562-c86d915ad1df4bbbmspans

对象分配的时候,根据对象的大小选择大小相近的span

spans 与 mcache 的关系如下图所示

20210129154022
mspans
// mSpanList heads a linked list of spans.
// 指向spans链表
//go:notinheap
type mSpanList struct {
first *mspan // first span in list, or nil if none
last *mspan // last span in list, or nil if none
}
 
//go:notinheap
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
list *mSpanList // For debugging. TODO: Remove.
 
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
 
manualFreeList gclinkptr // list of free objects in mSpanManual spans
 
freeindex uintptr
 
nelems uintptr // number of object in the span.
 
allocCache uint64
 
allocBits *gcBits
gcmarkBits *gcBits
 
// sweep generation:
// if sweepgen == h->sweepgen - 2, the span needs sweeping
// if sweepgen == h->sweepgen - 1, the span is currently being swept
// if sweepgen == h->sweepgen, the span is swept and ready to use
// if sweepgen == h->sweepgen + 1, the span was cached before sweep began and is still cached, and needs sweeping
// if sweepgen == h->sweepgen + 3, the span was swept and then cached and is still cached
// h->sweepgen is incremented by 2 after every GC
 
sweepgen uint32
divMul uint16 // for divide by elemsize - divMagic.mul
baseMask uint16 // if non-0, elemsize is a power of 2, & this will get object allocation base
allocCount uint16 // number of allocated objects
spanclass spanClass // size class and noscan (uint8)
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
needzero uint8 // needs to be zeroed before allocation
divShift uint8 // for divide by elemsize - divMagic.shift
divShift2 uint8 // for divide by elemsize - divMagic.shift2
elemsize uintptr // computed from sizeclass or from npages
limit uintptr // end of data in span
speciallock mutex // guards specials list
specials *special // linked list of special records sorted by offset.
}

 

mSpanList 是一个mspans链表,这个很好理解。重点看下 mspan 结构体

每个 mspan 都对应两个位图标记:mspan.allocBits 和 mspan.gcmarkBits

(1)allocBits中每一位用于标记一个对象存储单元是否已分配。

allocBits

(2)gcmarkBits中每一位用于标记一个对象是否存活。

gcMarkBits

03. Golang中GC的三色标记

mcentral

mentral 是一个空闲列表。

实际上 mcentral 它并不包含空闲对象列表,真正包含的是 mspan 。

每个mcentral 是两个 mspans 列表:空闲对象 c->notempty 和 完全分配对象 c->empty,如图所示

mcentral

当申请一个 16b 大小的内存时,如果 p.mcache 中无可用大小内存时,则它找一个最合适的规则 mcentral 查找,如图所示这时会在存放16b大小的 mcentral 中的 notempty 里查找。

文件源码:https://github.com/golang/go/blob/go1.16.2/src/runtime/mcentral.go

 

type mcentral struct {
spanclass spanClass
partial [2]spanSet // list of spans with a free object
full [2]spanSet // list of spans with no free objects
}

 

 

其中 partial 和 full 都包含两个 spans 集数组。一个用在扫描 spans,另一个用在未扫描spans。在每轮GC期间都扮演着不同的角色。mheap_.sweepgen 在每轮gc期间都会递增2。

partial 和 full 的数据类型为 spanSet,表示 *mspans 集。

type spanSet struct { spineLock mutex spine unsafe.Pointer // *[N]*spanSetBlock, accessed atomically spineLen uintptr // Spine array length, accessed atomically spineCap uintptr // Spine array cap, accessed under lock   index headTailIndex }

对 mcentral 的初始化如下

// Initialize a single central free list.
func (c *mcentral) init(spc spanClass) {
c.spanclass = spc
lockInit(&c.partial[0].spineLock, lockRankSpanSetSpine)
lockInit(&c.partial[1].spineLock, lockRankSpanSetSpine)
lockInit(&c.full[0].spineLock, lockRankSpanSetSpine)
lockInit(&c.full[1].spineLock, lockRankSpanSetSpine)
}

 

 

mheap

还是上面的例子,假如申请 16b 内存时,依次经过 mcache 和 mcentral 都没有可用适宜规则的大小内存,这时候会向 mheap 申请一块内存。然后按指定规格划分为一些列表,并将其添加到相同规格大小的 mcentral 的 not empty list 后面;

mheap

Go 没法使用工作线程的本地缓存 mcache 和全局中心缓存 mcentral 上管理超过32KB的内存分配,所以对于那些超过32KB的内存申请,会直接从堆上(mheap)上分配对应的数量的内存页(每页大小是8KB)给程序。

type mheap struct {
// lock must only be acquired on the system stack, otherwise a g
// could self-deadlock if its stack grows with the lock held.
lock mutex
pages pageAlloc // page allocation data structure
sweepgen uint32 // sweep generation, see comment in mspan; written during STW
sweepdone uint32 // all spans are swept
sweepers uint32 // number of active sweepone calls
 
 
allspans []*mspan // all spans out there
 
_ uint32 // align uint64 fields on 32-bit for atomics
 
// Proportional sweep
pagesInUse uint64 // pages of spans in stats mSpanInUse; updated atomically
pagesSwept uint64 // pages swept this cycle; updated atomically
pagesSweptBasis uint64 // pagesSwept to use as the origin of the sweep ratio; updated atomically
sweepHeapLiveBasis uint64 // value of heap_live to use as the origin of sweep ratio; written with lock, read without
sweepPagesPerByte float64 // proportional sweep ratio; written with lock, read without
 
scavengeGoal uint64
 
// Page reclaimer state
// This is accessed atomically.
reclaimIndex uint64
 
// This is accessed atomically.
reclaimCredit uintptr
 
 
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
 
 
heapArenaAlloc linearAlloc
 
 
arenaHints *arenaHint
 
 
arena linearAlloc
 
 
allArenas []arenaIdx
 
sweepArenas []arenaIdx
 
 
markArenas []arenaIdx
 
 
curArena struct {
base, end uintptr
}
 
_ uint32 // ensure 64-bit alignment of central
 
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
 
spanalloc fixalloc // allocator for span*
cachealloc fixalloc // allocator for mcache*
specialfinalizeralloc fixalloc // allocator for specialfinalizer*
specialprofilealloc fixalloc // allocator for specialprofile*
speciallock mutex // lock for special record allocators.
arenaHintAlloc fixalloc // allocator for arenaHints
 
unused *specialfinalizer // never set, just here to force the specialfinalizer type into DWARF
}

 

var mheap_ mheap

对于 mheap.arenas 字段对应 heapArena类型, 用来存储 heap arena 元数据,存储在Go堆的外部,并通过 mheap.arenas 索引进行访问。

// A heapArena stores metadata for a heap arena. heapArenas are stored
// outside of the Go heap and accessed via the mheap_.arenas index.
//
//go:notinheap
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan
 
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
checkmarks *checkmarksMap
 
zeroedBase uintptr
}

 

  HeapArena.bitmap HeapArena.spans

 

基于 HeapArena 记录的元数据信息,我们只要知道一个对象的地址,就可以根据 HeapArena.bitmap 信息扫描它内部是否含有指针;也可以根据对象地址计算出它在哪一页,然后通过 HeapArena.spans 信息查到该对象存在哪一个mspan中。

对于heap结构中的字段比较多,有几个使用频率非常高的字段,如 allspansarenasallArenassweepArenasmarkArenas 和 central 。有些是与GC 有关,有些是与内存维护管理有关。随着阅读runtime的时间,会越来越了解每个字段的使用场景。

参考资料

标签:span,mcentral,mcache,内存,spans,mspan,mheap
来源: https://www.cnblogs.com/shiqi17/p/16260438.html