0%

《Golang》Golang内存管理

在分析Golang的内存管理之前,需要先了解下Google 开发的内存分配器TCMalloc,Golang大致沿用了它的思路

TCMalloc(Thread-Caching Malloc)

TCMalloc 是 Google 开发的内存分配器,在不少项目中都有使用,例如在 Golang 中就使用了类似的算法进行内存分配。

它具有现代化内存分配器的基本特征:对抗内存碎片、在多核处理器能够 scale。

据称,它的内存分配速度是 glibc2.3 中实现的 malloc的数倍。

上图是三大组件的结构图

ThreadCache

假设所有的内存分配都有一个中心的堆分配的话,如果两个并行线程同时需要申请内存,这时候必然导致中心堆加锁,另一个线程等待锁,导致效率低下

所以出现了ThreadCache,每一个线程都有一个ThreadCache,各线程的ThreadCache连接成一个双向链表,当线程需要内存时,向自己的ThreadCache申请

ThreadCache中是由多种规格的freelist组成,从上图看,分成了8byte、16byte、32byte…256k等规格。freelist是一个由规格内存块组成的单向链表,每个单元的前8个字节作为节点指针,指向下一个单元

下面是规格的划分规则:

  • 16字节以内,每8字节划分一个size class。
    • 满足这种情况的size class只有两个:8字节、16字节。
  • 16~128字节,每16字节划分一个size class。
    • 满足这种情况的size class有7个:32, 48, 64, 80, 96, 112, 128字节。
  • 128B~256KB,按照每次步进(size / 8)字节的长度划分,并且步长需要向下对齐到2的整数次幂,比如:
    • 144字节:128 + 128 / 8 = 128 + 16 = 144
    • 160字节:144 + 144 / 8 = 144 + 18 = 144 + 16 = 160
    • 176字节:160 + 160 / 8 = 160 + 20 = 160 + 16 = 176
    • 以此类推

256k大小的内存块,我们称作一个page,当线程申请小于page大小的内存时,可以先从ThreadCache中获取。

但如果要申请256k以上内存时,ThreadCache拿不出来这么大的内存块,因为没有大于256k的规格的freelist

或者ThreadCache中这个规格的freelist没有可用元素了(会从CentralCache中拿内存生成一批这个规格的元素塞进这个规格的freelist)

这时候就要靠Pageheap了

Pageheap

Pageheap是一个所有线程公用的内存池,两个并行线程同时申请超过256k的内存,将会有锁等待

这里引入span的概念

多个连续的 Page 会组成一个 Span,在 Span 中记录起始 Page 的编号,以及 Page 数量

Pageheap与ThreadCache类似,分成了1page的span、2page的span…128page的span多种规格,每个规格的span形成spanlist单向链表

超过128个page的span,存储于一个有序set(std::set)

所有spanlist组成了CentralCache,也就是说每种span规格都有一个CentralCache

在一个CentralCache中,我们用链表把所有同规格Span组织起来,每次需要分配时就找一个 Span 从中分配一个 Object;当没有空闲的 Span 时,就从 PageHeap 申请 Span,PageHeap也不够的话,就通过系统调用向操作系统申请

Golang内存管理 (早期Golang版本,新版本会有些出入)

Go在程序启动的时候,运行时会先使用mmap系统调用向操作系统申请一块内存(注意这时还只是一段虚拟地址空间,并不会真正地分配物理内存),切成小块后自己进行管理。

C运行时同样是这么干的,先通过mmap向操作系统批发堆(物理地址空间)并管理它,然后通过暴露malloc函数提供申请内存(虚拟地址空间)的功能,当发现之前批发的物理空间不够用了,则再次通过mmap批发一些物理空间

这样做是防止程序频繁的使用mmap系统调用,大大影响性能

申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB大小。

arena区域就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成8KB大小的页,一些页组合起来称为mspan。

bitmap区域标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。bitmap中一个byte大小的内存对应arena区域中4个指针大小(指针大小为 8B )的内存,所以bitmap区域的大小是512GB/(4*8B)=16GB。

bitmap的高地址部分指向arena区域的低地址部分,也就是说bitmap的地址是由高地址向低地址增长的。

spans区域存放mspan的指针,每个指针对应一页,所以spans区域的大小就是512GB/8KB*8B=512MB(除以8KB是计算arena区域的页数,而最后乘以8是计算spans区域所有指针的大小)。

创建mspan的时候,按页填充对应的spans区域,在回收object时,根据地址很容易就能找到它所属的mspan。

mcache

mcache类似于ThreadCache,每个工作线程都会绑定一个mcache

mcache在初始化的时候是没有任何mspan资源的,在使用过程中会动态地从mcentral申请,之后会缓存下来。当对象小于等于32KB大小时,使用mcache的相应规格的mspan进行分配。

mcentral

mcentral类似于CentralCache

为所有mcache提供切分好的mspan资源。每个central保存一种特定大小的全局mspan列表,包括已分配出去的和未分配出去的。 每个mcentral对应一种mspan,而mspan的种类导致它分割的object大小不同。当工作线程的mcache中没有合适(也就是特定大小的)的mspan时就会从mcentral获取

empty表示这条链表里的mspan都被分配了object,或者是已经被cache取走了的mspan,这个mspan就被那个工作线程独占了。

而nonempty则表示有空闲对象的mspan列表。每个central结构体都在mheap中维护。

简单说下mcache从mcentral获取和归还mspan的流程:

  • 获取

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

    • 加锁;将mspan从empty链表删除;将mspan加入到nonempty链表;
    • 解锁。

mheap

mheap类似于Pageheap,代表Go程序持有的所有堆空间,Go程序使用一个mheap的全局对象_mheap来管理堆内存。

当mcentral没有空闲的mspan时,会向mheap申请。而mheap没有资源时,会向操作系统申请新内存。mheap主要用于大对象的内存分配,以及管理未切割的mspan,用于给mcentral切割成小对象。

mheap中含有所有规格(class size)的mcentral,所以,当一个mcache从mcentral申请mspan时,只需要在独立的mcentral中使用锁,并不会影响申请其他规格的mspan。

上图我们看到,bitmap和arena_start指向了同一个地址,这是因为bitmap的地址是从高到低增长的,所以他们指向的内存位置相同。

每个Size Class有两个mspan,也就是有两个Span Class。其中一个分配给含有指针的对象,另一个分配给不含有指针的对象。这会给垃圾回收机制带来利好

也就是说mheap的central数组(存放所有规格的mcentral)中,每种规格都有两份mcentral,一份负责含有指针的对象,另一份负责不含有指针的对象

mspan

mspan类似于TCMalloc中的span

Go中内存管理的基本单元,是由一片连续的8KB的页组成的大块内存。注意,这里的页和操作系统本身的页并不是一回事,它一般是操作系统页大小的几倍。一句话概括:mspan是一个包含起始地址、mspan规格、页的数量等内容的双端链表。

在Golang启动过程的文章中学到过schedinit函数,其中调用了mallocinit函数,我们看看

分配流程

Go的内存分配器在分配对象时,根据对象的大小,分成三类:

  1. 小对象(小于等于16B)
  2. 一般对象(大于16B,小于等于32KB)
  3. 大对象(大于32KB)。

大体上的分配流程:

  • > 32KB的对象,直接从mheap上分配;
  • <=16B 的对象使用mcache的tiny分配器分配;
    • 如果mcache没有相应规格大小的mspan,则向mcentral申请
    • 如果mcentral没有相应规格大小的mspan,则向mheap申请
    • 如果mheap中也没有合适大小的mspan,则向操作系统申请
  • (16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
    • 如果mcache没有相应规格大小的mspan,则向mcentral申请
    • 如果mcentral没有相应规格大小的mspan,则向mheap申请
    • 如果mheap中也没有合适大小的mspan,则向操作系统申请

下篇预告

Golang内存管理源码解析




微信关注我,及时接收最新技术文章