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是否包含指针等位图, 这样做的主要原因是为了更高效的利用内存: 分配、回收和再利用;