1. 设计思想
Go语言的内存分配算法参考了谷歌的TCMalloc(线程缓存分配Thread-Caching Malloc)算法,它的核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。简单的说就是维护一块大的全局内存,每个线程维护一块小的私有内存,申请内存时优先从私有内存中分配,私有内存不足再加锁从全局内存申请。这种算法减少了系统调用并且避免了不同线程对全局内存池的频繁竞争。
Go在程序启动的时候,会先向操作系统申请一块内存,切成小块后自己进行管理。申请到的内存块被划分为三个区域arena、spans、bitmap,在X64上分别是512GB,16GB,512MB大小。
arena就是堆区,它把内存分割成8KB大小的页,一些页组合起来称为mspan,程序中需要的内存就从这里分配。
spans区域存放mspan的指针,每个指针对应mspan的一页,在回收对象时,根据地址能很容易就能找到它所属的mspan。
bitmap区域标识了arena中哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。主要用于GC内存回收。
2. 内存分配组件
- mspan
Go中内存管理的基本单元,是由一片连续的8KB的页组成的大块内存。mspan之间会构成双向链表。每个mspan按照Size Class的大小分割为若干个object,每个object可存储一个对象,并会用一个位图来标记尚未使用的object。 - mcache
每个工作线程都会绑定一个mcache,用于在本地缓存可用的mspan,降低了锁资源的消耗。从图中可以看出,mcache中有一个数组list,每个数组单元挂一串链表,同一链表的节点大小相同,节点即可用内存,节点大小由数组下标决定,数组下标就代表不同的Size Class。除去内存分配外,mcache上还有很多状态计数器,主要用来统计内存分配情况。
mcache的分配过程:根据Size Class参数从list中取出对应的内存块链表,若链表不为空,则返回可用节点;若为空,就要去mcentral中申请一些此规格的缓存,再返回一个可用节点。
mcache释放内存的条件:a,某个内存链表过长(>=256 )时,会截取此链表的一部分节点,返还给mcentral;b,整个mcache缓存过大(>=1M),同样将每个链表返还部分节点给mcentral。
- mcentral
所有线程共享的内存池,因此访问时要加锁。一个mcentral对应一个Size Class,当mcache中没有合适大小的mspan时就会从mcentral获取。mcentral被所有的线程共同享有,存在竞争,因此会消耗锁资源。 - mheap
代表Go程序持有的所有堆空间。当mcentral中没有可用的mspan时,就会向mheap申请。而mheap没有资源时,会向操作系统申请新内存。
mheap主要用于大对象的内存分配,以及管理未切割的mspan,用于给mcentral切割成不同规格的mspan。
根据规格从从mheap分配mspan给mcentral。如果规格大到链表中没有合适的mspan,就只能从OS中去申请,填充heap,再试图分配。拿到的mspan所含的page数目大于了请求的page数目,并不会将这个span返回,而是将其拆分成两个mspan,将剩余mspan重新放回mheap中去。
3. 分配规则
变量是在栈上分配还是在堆上分配,是由逃逸分析的结果决定的。通常情况下,编译器是倾向于将变量分配到栈上的,因为它的开销小,如果所有的变量都在栈上分配,那么就不会存在内存碎片,垃圾回收之类的东西。
Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。
- 大对象,直接从mheap上分配;
- 小对象,使用mcache的tiny分配器分配;
- 一般对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
- 如果mcache没有相应规格大小的mspan,则向mcentral申请
- 如果mcentral没有相应规格大小的mspan,则向mheap申请
- 如果mheap中也没有合适大小的mspan,则向操作系统申请
4. 总结
- Go程序启动时向操作系统申请一块内存,并划分成spans、bitmap、arena区域。arena区域按8kb大小的页划分成一个个小块,若干页组合形成mspan。spans存mspan的指针,bitmap标识内存情况,主要用于垃圾回收。
- mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。
- 极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般对象通过mspan分配内存;大对象则直接由mheap分配内存。