Go 内存分配机制?

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

设计思想

  • 内存分配算法采用Google的TCMalloc算法,每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向加锁向全局内存池申请,减少系统调用并且避免不同线程对全局内存池的锁竞争
  • 把内存切分的非常的细小,分为多级管理,以降低锁的粒度
  • 回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,才会尝试归还部分内存给操作系统,降低整体开销

分配组件

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

555.Go 内存分配机制? - 图1

内存管理单元:mspan

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

page是内存存储的基本单元,“对象”放到page

  1. type mspan struct {
  2. next *mspan // 后指针
  3. prev *mspan // 前指针
  4. startAddr uintptr // 管理页的起始地址,指向page
  5. npages uintptr // 页数
  6. spanclass spanClass // 规格
  7. ...
  8. }
  9. type spanClass uint8

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

  1. const _NumSizeClasses = 68
  2. 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}

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

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

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

555.Go 内存分配机制? - 图2

字段含义:

  • 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字段

  1. type mcache struct {
  2. alloc [numSpanClasses]*mspan
  3. }
  4. _NumSizeClasses = 68
  5. 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结构内

  1. type mcentral struct {
  2. spanclass spanClass // 指当前规格大小
  3. partial [2]spanSet // 有空闲object的mspan列表
  4. full [2]spanSet // 没有空闲object的mspan列表
  5. }

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

  1. type spanSet struct {
  2. spineLock mutex
  3. spine unsafe.Pointer // 指向[]span的指针
  4. spineLen uintptr // Spine array length, accessed atomically
  5. spineCap uintptr // Spine array cap, accessed under lock
  6. index headTailIndex // 前32位是头指针,后32位是尾指针
  7. }

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

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

页堆:mheap

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

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

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

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

分配对象

  • 微对象 (0, 16B):先使用线程缓存上的微型分配器,再依次尝试线程缓存、中心缓存、堆 分配内存;
  • 小对象 [16B, 32KB]:依次尝试线程缓存、中心缓存、堆 分配内存;
  • 大对象 (32KB, +∞):直接尝试堆分配内存;

分配流程

  • 首先通过计算使用的大小规格
  • 然后使用mcache中对应大小规格的块分配。
  • 如果mcentral中没有可用的块,则向mheap申请,并根据算法找到最合适的mspan
  • 如果申请到的mspan 超出申请大小,将会根据需求进行切分,以返回用户所需的页数。剩余的页构成一个新的 mspan 放回 mheap 的空闲列表。
  • 如果 mheap 中没有可用 span,则向操作系统申请一系列新的页(最小 1MB)

555.Go 内存分配机制? - 图3