Golang 内存之mspan、mcache、mcentral 和 mheap 数据结构

本文最新版本请查看原文:https://blog.haohtml.com/archives/29385

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

components of memory allocation

golang 内存分配组件

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

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

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

  • <16b 会使用微小对象内存分配器,主要使用 mcache.tinyXXX 这类的字段
  • 16-32b 从P下面的 mcache 中分配
  • >32b 直接从 mheap 中分配

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


Golang内存组件关系

mcache

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

在 Go 1.2 版本前调度器使用的是 GM 模型,将mcache 放在了 M 里,但发现存在诸多问题,期中对于内存这一块存在着巨大的浪费。每个M 都持有 mcachestack 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
}
  • nextSample 分配多少大小的堆时触发堆采样;
  • scannAlloc 分配的可扫描堆字节数;
  • tiny 堆指针,指向当前 tiny 块的起始指针,如果当前无tiny块则为nil。在终止标记期间,通过调用 mcache.releaseAll() 来清除它;
  • tinyoffset 当前tiny 块的位置;
  • tinyAllocs 拥有当前 mcache 的 P 执行的微小分配数;
  • alloc [numSpanClasses]*mspan 当前P的分配规格信息,共 numSpanClasses = _NumSizeClasses << 1 种规格
  • stackcache 内存规格序号,按 spanClass 索引,参考这里;
  • flushGen 表示上次刷新 mcachesweepgen(清扫生成)。如果 flushGen != mheap_.sweepgen 则说明 mcache 已过期需要刷新,需被被清扫。在 acrequirep 中完成;

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

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

mspans

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

spansmcache 的关系如下图所示

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 结构体

  • next 指向下一个span的指针,为nil表示没有
  • prev 指向上一个span的指针,与next相反
  • list 指向mSpanList,调试使用,以后会废弃
  • startAddr span第一个字节地址,可通过 s.base() 函数读取
  • npages span中的页数(一个span 是由多个page组成的,与linux中的页不是同一个概念)
  • manualFreeListmSpanManual spans中的空闲对象的列表
  • freeindex 标记0~nelems之间的插槽索引,标记的的是在span中的下一个空闲对象;
    每次分配内存都从 allocBitsfreeindex 索引位置开始,直到遇到 0 ,表示空闲对象,然后调整 freeindex 使得下一次扫描能跳过上一次的分配;
    freeindex==nelem,则当前span没有了空余对象;
    allocBits 是对象在span中的位图;
    如果 n >= freeindex and allocBits[n/8] & (1<<(n%8)) == 0 , 那么对象n是空闲的;
    否则,对象 n 表示已被分配。从 elem 开始的是未定义的,将不应该被定义;
  • nelems span中对象数(page是内存存储的基本单元, 一个span由多个page组成,同时一个对象可能占用一个或多个page)
  • allocCachefreeindex 位置的 allocBits 缓存
  • allocBits 标记span中的elem哪些是被使用的,哪些是未被使用的;清除后将释放 allocBits ,并将 allocBits 设置为 gcmarkBits
  • gcmarkBits 标记span中的elem哪些是被标记了的,哪些是未被标记的

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
}
  • spanClass 指当前规格大小
  • partial 存在空闲对象spans列表
  • full 无空闲对象spans列表

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

partialfull 的数据类型为 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 内存时,依次经过 mcachemcentral 都没有可用适宜规则的大小内存,这时候会向 mheap 申请一块内存。然后按指定规格划分为一些列表,并将其添加到相同规格大小的 mcentralnot 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
  • lock 全局锁,保证并发,所以尽量避免从mheap中分配
  • pages 页面分配的数据结构
  • sweepgen 清扫生成
  • sweepdone 清扫完成标记
  • sweepers 活动清扫调用 sweepone 数
  • allspans 所有的 spans 都是通过 mheap_ 申请,所有申请过的 mspan 都会记录在 allspans。结构体中的 lock 就是用来保证并发安全的。
  • pagesInUse 统计mSpanInUse 中spans的页数
  • pagesSwept 本轮清扫的页数
  • pagesSweptBasis 用作清扫率
  • sweepHeapLiveBasis 用作扫描率的heap_live 值
  • sweepPagesPerByte 清扫率
  • scavengeGoal 保留的堆内存总量(预先设定的),runtime 将试图返还内存给OS
  • reclaimIndex 指回收的下一页在allAreans 中的索引。具体来说,它指的是 arena allArenas[i/pagesPerArena] 的第(i%pagesPerArena)页
  • reclaimCredit 多余页面的备用信用。因为页回收器工作在大块中,它可能回收的比请求的要多,释放的任何备用页将转到此信用池
  • arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena 堆arena 映射。它指向整个可用虚拟地址空间的每个 arena 帧的堆元数据;
    使用arenaIndex将索引计算到此数组中;
    对于没有Go堆支持的地址空间区域,arena映射包含nil
    一般来说,这是一个两级映射,由一个L1级映射和多个L2级映射组成;
    当有大量的的 arena 帧时将节省空间,然而在许多平台(64位),arenaL1Bits 是0,这实际上是一个单级映射。这种情况下arenas[0]永远不会为零。
  • heapArenaAlloc 是为分配heapArena对象而预先保留的空间。仅仅用于32位系统。
  • arenaHints 试图添加更多堆 arenas 的地址列表。它最初由一组通用少许地址填充,并随实 heap arena 的界限而增长。
  • arena
  • allArenas []arenaIdx 是每个映射arena的arenaIndex 索引。可以用以遍历地址空间。
  • sweepArenas []arenaIdx 指在清扫周期开始时保留的 allArenas 快照
  • markArenas []arenaIdx 指在标记周期开始时保留的 allArenas 快照
  • curArena 指heap当前增长时的 arena,它总是与physPageSize对齐。
  • central 重要字段!这个就是上面介绍的 mcentral ,每种规格大小的块对应一个 mcentral。pad 是一个字节填充,用来避免伪共享(false sharing)
  • spanalloc 数据类型 fixalloc 是 free-list,用来分配特定大小的块。比如 cachealloc 分配 mcache 大小的块。
  • cachealloc 同上
  • 其它

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

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,548评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,178评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,844评论 2 7