Golang内存分配机制
Go语言内置运行时(就是runtime), 抛弃了传统的内存分配方式, 改为自主管理。这样可以自主实现更好的内存使用模式, 比如内存池、预分配等等。这样, 不会每次内存分配都需要系统调用;
设计思想
- 内存分配算法采用Google的
TCMalloc算法, 每个线程都会自行维护一个独立的内存池, 进行内存分配时优先从该内存池中分配, 当内存池不足时才会向加锁的全局内存池申请, 减少系统调用并且避免不同线程对全局内存池的锁竞争; - 把内存切分的非常细小, 分为多级管理, 以降低锁的粒度;
- 回收对象时, 并没有将其真正释放, 只是放回预先分配的大块内存中, 以便复用。只有内存闲置过多的时候, 才会尝试归还部分内存给操作系统, 降低整体开销;
分配组件
Go的内存管理组件主要有:
mspan、mcache、mcentral和mheap;
内存管理单元: mspan
mspan是内存管理的基本单元, 该结构体中包含next和prev两个字段, 它们分别指向前一个和后一个mspan, 每个mspan都管理npages个大小为8kb的页, 一个span是由多个page组成, 这里的页不是操作系统中的内存页, 它们是操作系统内存页的整数倍;
page是内存存储的基本单元, “对象”放到page中
1 | type mspan struct { |
Go有68种不同大小的spanClass, 用于小对象分配
1 | const _NumSizeClasses = 68 |
如果按照序号为1的spanClass(对象规格为8B)分配, 每个span占用堆的字节数: 8k, mspan可以保存1024个对象;
如果按照序号为2的spanClass(对象规格为16B)分配, 每个span占用堆的字节数: 8k, mspan可以保存512个对象;
…
如果按照序号为67的spanClass(对象规格为32K)分配, 每个span占用堆的字节数: 32k, mspan可以保存1个对象;
| class | bytes/obj | bytes/span | objects | tail waste | max waste |
|---|---|---|---|---|---|
| 1 | 8 | 8192 | 1024 | 0 | 87.50% |
| 2 | 16 | 8192 | 512 | 0 | 43.75% |
| 3 | 24 | 8192 | 341 | 0 | 29.24% |
| 4 | 32 | 8192 | 256 | 0 | 46.88% |
| 5 | 48 | 8192 | 170 | 32 | 31.52% |
| 6 | 64 | 8192 | 128 | 0 | 23.44% |
| 7 | 80 | 8192 | 102 | 32 | 19.07% |
| … | … | … | … | … | … |
| 67 | 32768 | 8192 | 1 | 0 | 12.50% |
字段含义:
- 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 { |
mcache和Span Classes作为索引管理多个用于分配的mspan, 它包含所有规格的mspan。它是_NumSizeClasses的2倍, 也就是68*2=136, 其中*2是将spanClass分成了有指针和没有指针两种, 方便于垃圾回收。对于每种规格, 有2个mspan, 一个不包含指针, 另一个则包含指针。对于无指针对象的mspan在进行垃圾回收时无需进一步扫描它是否引用了其他活跃的对象。
mcache在初始化的时候是没有任何mspan资源的, 在使用过程种会动态地从mcentral申请, 之后会缓存下来。当对象小于等于32KB时, 使用mcache的相应规格的mspan进行分配。
中心缓存: mcentral
mcentral管理全局的mspan供所有线程使用, 全局mheap变量包含central字段, 每个mcentral结构都维护在mheap结构内;
1 | type mcentral struct { |
每个mcentral管理一种spanClass的span, 并将有空闲空间和没有空闲空间的mspan分开管理。partail和full的数据类型为spanSet, 表示mspans集, 可用通过pop、push来获得mspans;
1 | type spanSet struct { |
简单说下mcache从mcentral获取和归还mspan的流程:
- 获取: 加锁, 从
partail链表找到一个可用的mspan; 并将其从partail链表删除; 将取出的mspan加入到full链表中; 将mspan返回给工作流程, 解锁; - 归还: 加锁, 将
mspan从full链表中删除, 将mspan加入到partail链表中, 解锁;
页堆: mheap
mheap管理Go所有动态分配内存, 可以认为是Go程序持有整个堆空间, 全局唯一;
1 | var mheap_ mheap |
所有mcentral的集合则是存放于mheap中的。mheap里的arena区域是堆内存的抽象, 运行时会将8KB看做一页, 这些内存页存储了所有在堆上初始化的对象。运行时使用二维的runtime.heapArena数组管理所有的内存, 每个runtime.heapArena都会管理64MB的内存;
当申请内存时, 依次经过mcache和mcentral都没有可用合适规格的大小内存, 这时候会向mheap申请一块内存。然后按指定规格划分为一些列表, 并将其添加到相同规格大小的mcentral的非空闲列表后面;
分配对象
- 微对象(0, 16B): 先使用线程缓存上的微型分配器, 再依次尝试线程缓存、中心缓存、堆 分配内存;
- 小对象(16B, 32KB): 一次尝试线程缓存、中心缓存、堆 分配内存;
- 大对象(32KB, +∞): 直接尝试堆分配内存;
分配流程
- 首先通过计算使用的大小规格;
- 然后使用
mcache中对应大小规格的块分配; - 如果
mcentral中没有可用的块, 则向mheap申请, 并根据算法找到最合适的mspan; - 如果申请到的
mspan超出申请大小, 将会根据需求进行切分, 以返回用户所需的页数, 剩余的页构成一个新的mspan放回mheap的空闲列表中; - 如果
mheap中没有可用的span, 则向操作系统申请一系列新的页(最小1MB);
Golang内存管理-其二
TCMalloc
go内存管理是借鉴了
TCMalloc的设计思想,TCMalloc全称Thread-Caching Malloc, 是Google开发的内存分配器。
Page
操作系统对内存管理以页为单位, TCMalloc也是这样, 只不过TCMalloc里的Page大小与操作系统大小并不一定相等, 而是倍数关系;
Span
一组连续的Page被称为Span, 比如可以有4个页大小的Sapn, 也可以有8个页大小的Span, Span比Page高一个层级, 是为了方便管理一定大小的内存区域, Span是TCMalloc中内存管理的基本单位;
ThreadCache
每个线程各自的Cache, 一个Cache包含多个空闲内存块链表, 每个链表连接的都是内存块, 同一个链表上的内存块的大小是相同的, 也可以说按内存块大小给内存块分了个类, 这样可以根据申请的内存大小, 快速从合适的链表选择空闲内存块, 由于每个线程有自己的ThreadCache, 所以ThreadCache访问是无锁的;
CentalCache
是所有线程共享的缓存, 也是保存的空闲内存块链表, 链表的数量与ThreadCache中链表数量相同, 当ThreadCache内存块不足时, 可以从CentralCache中取; 当ThreadCache内存块多时, 可以放回CentralCache中; 由于CentralCache是共享的, 所有它的访问是要加锁的;
PageHeap
PageHeap是堆内存的抽象, PageHeap存的也是若干链表, 链表保存的是Span, 当CentralCache没有内存时, 会从PageHeap中取, 把1个Span拆成若干内存块, 添加到对应大小的链表中, 当CentralCache内存多的时候, 会放回PageHeap中;
TCMalloc对象分配
小对象直接从ThreadCache中分配, 若ThreadCache不够则从CentralCache中获取内存, CentralCache内存不够时再从PageHeap中获取内存, 大对象在PageHeap中选择合适的页组成Span用于存储数据;
Go内存管理
经过上述对TCMalloc内存管理的描述, 接着看一下Go内存管理架构图
Page
和TCMalloc中的Page相同, 图中最下方浅蓝色长方形代表一个Page;
Span
与TCMalloc中的Span相同, Span是go内存管理的基本单元, 代码中为mspan, 一组连续的Page组成1个Span, 所以上图一组连续的浅蓝色长方形代表的是一组Page组成的1个Span, 另外, 1个淡紫色长方形为1个Span;
mcache
mcache与TCMalloc中的ThreadCache类似, mcache保存的是各种大小的Span, 并按Span class分类, 小对象直接从mcache分配内存, 它起到了缓存的作用, 并且可以无锁访问。但mcache与ThreadCache也有不同点, TCMalloc中是每个线程1个ThreadCache, Go中是每个P拥有1个mcache, 因为在Go程序中, 当前最多有GOMAXPROC个线程运行, 所以最多需要GOMAXPROCS个mcache就可以保证各线程对mache的无锁访问, 下图是G, P, M三者之间的关系:
mcentral
mcentral与TCMalloc中的CentralCache类似, 是所有线程共享的缓存, 需要加锁访问, 它按Span class对Span分类, 串联成链表, 当mcache的某个级别Span的内存被分配光时, 它会向mcentral申请1个当前级别的Span。但mcentral与CentralCache也有不同点, CentralCache是每个级别的Span有1个链表, mcache是给个级别的Span有2个链表;
mheap
mheap与TCMalloc中的PageHeap类似, 它是堆内存的抽象, 把从OS(系统)申请出的内存页组织成Span, 并保存起来。当mcentral的Span不够用时会向mheap申请, mheap的Span不够用时会向OS申请, 向OS的内存申请是按页来的, 然后把申请来的内存页生成Span组织起来, 同样也是需要加锁访问的。但mheap与PageHeap也有不同点: mhep把Span组织成了树结构, 而不是链表, 并且还是两棵树, 然后把Span分配到heapArena进行管理, 它包含地址映射和Span是否包含指针等位图, 这样做的主要原因是为了更高效的利用内存: 分配、回收和再利用;





GOLANG ROADMAP · 知识星球