基础概念

为了方便自主管理内存,做法便是先向系统申请一块内存,然后将内存切割成小块,通过一定的内存分配算法管理内存。
以64位系统为例,Golang程序启动时会向系统申请的内存如下图所示:

预申请的内存划分为spans、bitmap、arena三部分。其中arena即为所谓的堆区,应用中需要的内存从这里分配。其中spans和bitmap是为了管理arena区而存在的。

  • arena的大小为512G,为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个页;
  • spans区域存放span的指针,每个指针对应一个或多个page,所以span区域的大小为(512GB/8KB)*指针大小8byte = 512M
  • bitmap区域大小也是通过arena计算出来,不过主要用于GC。

 Go内存管理的流程

go内存管理的简单流程如下

简单流程

我们的go进程需要申请一个小对象(<=32KB)的时候直接从mcache里面申请,

如果mcache里面没有多余的空间分配,就向mcentral申请一个单位的空间(xKB,具体大小先不管,后面会说)。如果mcentral没有多余的呢,就向mheap申请;如果mheap也不够了呢,mheap就直接从操作系统中分配一组新的内存空间(至少1MB)。

如果申请的大对象(>32KB),直接从mheap分配。

可以发现流程很简单,就是当自己需要内存就向上一级申请内存空间,如果没有多余,就自己上级模块再向他的上一级的内存模块申请空间,依次类推直到内核。

为了避免混乱,我们再看一张图来深化一下各个结构之间的关系:

可以看出 mheap为结构最顶层,包含arena中的mcentral 和 逻辑处理器(P)中的mcache

重要概念:

需要知道的结构名词有:

  • page
  • span
  • mcache
  • mentral
  • mheap

1.Page

操作系统内存管理中,内存的最粒度是4KB,也就是说分配内存最小4kb起。而golang里面一个page是8KB。

2.Span

mspan

3.Mcache

各线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断地加锁,Golang为每个线程分配了span的缓存,这个缓存即是cache。

mcache保存的是各种大小的Span,并按Span class分类,小对象(<=32KB)直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问

mcache是每个逻辑处理器(P)的本地内存线程缓存。Go中是每个P拥有1个mcache。

mcache中每个级别的Span有2类数组链表一个不带指针,一个带指针需要GC扫码。

4.Mcentral

它按Span class对Span分类,串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。所有线程共享的缓存,需要加锁访问。

5.Mheap

它把从OS申请出的内存页组织成Span,并保存起来。当mcentral的Span不够用时会向mheap申请,mheap的Span不够用时会向OS申请,向OS的内存申请是按页来的,然后把申请来的内存页生成Span组织起来,同样也是需要加锁访问的。大对象(>32KB)直接从mheap上分配。

总结

Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。

  1. Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域
  2. arena区域按页划分成一个个小块
  3. span管理一个或多个页
  4. mcentral管理多个span供线程申请使用
  5. mcache作为线程私有资源,资源来源于mcentral