面试官:谈谈 Go 内存分配策略

大家好,我是木川

Go语言内置运行时(就是runtime),抛弃了传统的内存分配方式,改为自主管理。这样可以自主地实现更好的内存使用模式,比如内存池、预分配等等。这样,不会每次内存分配都需要进行系统调用。

一、设计思想

  • 内存分配算法采用Google的TCMalloc算法,每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向加锁向全局内存池申请,减少系统调用并且避免不同线程对全局内存池的锁竞争

  • 把内存规格切分的比较细,分级管理,以降低锁的粒度

  • 回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,才会尝试归还部分内存给操作系统,降低整体开销

二、分配组件

Go的内存管理组件主要有:mspanmcachemcentralmheap

e47e9784410af736bfd9941012738b78.png

内存管理单元:mspan

mspan是 内存管理的基本单元,该结构体中包含 nextprev 两个字段,它们分别指向了前一个和后一个mspan,每个mspan 都管理 npages 个大小为 8KB 的 page,一个span 是由多个page组成的,这里的页不是操作系统中的内存页,它们是操作系统内存页的整数倍。page是内存存储的基本单元,“对象”放到page

type mspan struct {
 next *mspan // 后指针
 prev *mspan // 前指针
 startAddr uintptr // 管理页的起始地址,指向page
 npages    uintptr // 页数
 spanclass   spanClass // 规格
 ...
}

type spanClass uint8

Go 有 68 种不同大小的 spanClass,用于小对象的分配

const _NumSizeClasses = 68
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
4cae535fdbc9a2ce5c2da035f31d924a.png

如果按照序号为1的spanClass(对象规格为8B)分配,每个span占用堆的字节数:8k,mspan可以保存1024个对象

如果按照序号为2的spanClass(对象规格为16B)分配,每个span占用堆的字节数:8k,mspan可以保存512个对象

如果按照序号为67的spanClass(对象规格为32K)分配,每个span占用堆的字节数:32k,mspan可以保存1个对象

字段含义:

  • class:class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型

  • bytes/obj:该class代表对象的字节数

  • bytes/span:每个span占用堆的字节数,也即页数*页大小

  • objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)

  • waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)

大于32k的对象出现时,会直接从heap分配一个特殊的span,这个特殊的span的类型(class)是0,  只包含了一个大对象

线程缓存:mcache

mcache 管理线程在本地缓存的mspan,每个goroutine绑定的P都有一个mcache字段

type mcache struct {
    alloc [numSpanClasses]*mspan
}

_NumSizeClasses = 68
numSpanClasses = _NumSizeClasses << 1

mcacheSpan Classes作为索引管理多个用于分配的mspan,它包含所有规格的mspan。它是_NumSizeClasses的2倍,也就是68*2=136,其中*2是将spanClass分成了有指针和没有指针两种,方便与垃圾回收。对于每种规格,有2个mspan,一个mspan不包含指针,另一个mspan则包含指针。对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。

mcache在初始化的时候是没有任何mspan资源的,在使用过程中会动态地从mcentral申请,之后会缓存下来。当对象小于等于32KB大小时,使用mcache的相应规格的mspan进行分配。

中心缓存:mcentral

mcentral管理全局的mspan供所有线程使用,全局mheap变量包含central字段,每个 mcentral 结构都维护在mheap结构内

type mcentral struct {
 spanclass spanClass // 指当前规格大小

 partial [2]spanSet // 有空闲object的mspan列表
 full    [2]spanSet // 没有空闲object的mspan列表
}

每个mcentral管理一种spanClass的mspan,并将有空闲空间和没有空闲空间的mspan分开管理。partial和 full的数据类型为spanSet,表示 mspans集,可以通过pop、push来获得mspans

type spanSet struct {
    spineLock mutex
    spine     unsafe.Pointer // 指向[]span的指针
    spineLen  uintptr        // Spine array length, accessed atomically
    spineCap  uintptr        // Spine array cap, accessed under lock

    index headTailIndex  // 前32位是头指针,后32位是尾指针
}

简单说下mcachemcentral获取和归还mspan的流程:

  • 获取;加锁,从partial链表找到一个可用的mspan;并将其从partial链表删除;将取出的mspan加入到full链表;将mspan返回给工作线程,解锁。

  • 归还;加锁,将mspanfull链表删除;将mspan加入到partial链表,解锁。

页堆:mheap

mheap管理Go的所有动态分配内存,可以认为是Go程序持有的整个堆空间,全局唯一

var mheap_ mheap
type mheap struct {
    lock      mutex    // 全局锁
    pages     pageAlloc // 页面分配的数据结构
    allspans []*mspan // 所有通过 mheap_ 申请的mspans
  // 堆
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
 
  // 所有中心缓存mcentral
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }
    ...
}

所有mcentral的集合则是存放于mheap中的。mheap里的arena 区域是堆内存的抽象,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象。运行时使用二维的 runtime.heapArena 数组管理所有的内存,每个 runtime.heapArena 都会管理 64MB 的内存。

当申请内存时,依次经过 mcachemcentral 都没有可用合适规格的大小内存,这时候会向 mheap 申请一块内存。然后按指定规格划分为一些列表,并将其添加到相同规格大小的 mcentral非空闲列表 后面

三、分配流程

小对象 [0, 32KB]:依次尝试线程缓存、中心缓存、堆 分配内存

大对象 (32KB, +∞):直接尝试堆分配内存

发生内存逃逸的对象:直接尝试堆分配内存

具体介绍下小对象的分配流程:

  • 首先通过计算使用的大小规格

  • 然后使用线程缓存mcache中对应大小规格的块分配。

  • 如果中心缓存mcentral中没有可用的块,则向堆mheap申请,并根据算法找到最合适的mspan

  • 如果申请到的mspan 超出申请大小,将会根据需求进行切分,以返回用户所需的页数。剩余的页构成一个新的 mspan 放回 mheap 的空闲列表。

  • 如果 mheap 中没有可用 span,则向操作系统申请一系列新的页(最小 1MB)

c58e7d2a8cd6b763598a47b24af911d1.png

最后给自己的原创 Go 面试小册打个广告,如果你从事 Go 相关开发,欢迎扫码购买,目前 10 元买断,加下面的微信发送支付截图额外赠送一份自己录制的 Go 面试题讲解视频

c92a406bc83f413722714a22b53a2069.jpeg

3fa7f5fe7d3fe5c10c7d159da5398313.png

如果对你有帮助,帮我点一下在看或转发,欢迎关注我的公众号

猜你喜欢

转载自blog.csdn.net/caspar_notes/article/details/133781892
今日推荐