简介

Go 的内存分配器采用了多级内存分配模块的核心设计思想,旨在减少内存分配时锁的使用以及系统调用的次数。

在 Go中,内存管理分为栈内存管理和堆内存管理两种。栈上的内存由编译器管理,而堆上的内存则由程序管理,在运行期间进行申请和释放(垃圾回收)。变量分配在堆上还是栈上,与语法无关,主要取决于 Go 的逃逸分析。

堆上的内存空间主要用于存储较大的对象,或者需要在函数作用域外依然存活的对象。

在Go中,对于堆上内存的管理主要包括内存分配和垃圾回收两个过程。本文主要介绍 Go 中堆上内存分配的相关内容

内存分配

整体结构

Go 的内存分配器借鉴了 TCMalloc 分配器的设计思想

  • 一次性或者提前分配多级内存模块,减少内存分配时锁的使用和与操作系统的交互
  • 多尺度内存单元,减少内存分配产生碎片

一些概念

内存单元 mspan

Go 中内存管理的基本单元,是由一片连续的 8KB 的页组成的大块内存。这里的页和操作系统本身的页并不是同个概念,它一般是操作系统页大小的几倍。

每个 mspan 按照它 span class 的大小分割成若干个object.

span Class 等于3,object 大小就是32B。 32B大小的 object 可以存储对象大小范围在 17B~32B 的对象。而对于微小对象(小于16B),分配器会将其进行合并,将几个对象分配到同一个 object 中。

上图一组连续的浅蓝色长方形代表的是一组 Page 组成的1个 span

内存单元等级 span class

mspan 根据空间大小和分配对象的大小,被划分为 66 种等级,如下表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 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         32        8192      256           0     46.88%
//     4         48        8192      170          32     31.52%
//     5         64        8192      128           0     23.44%
//     6         80        8192      102          32     19.07%
//     7         96        8192       85          32     15.95%
//     8        112        8192       73          16     13.56%
//     9        128        8192       64           0     11.72%
//    10        144        8192       56         128     11.82%

//    ...
//    65      28672       57344        2           0      4.91%
//    66      32768       32768        1           0     12.50%

线程缓存 mcache

每个大小层级的 span 都会在 mcache 中保存一份

而不同的 mcache 对应不同的逻辑处理器P。由于每个 P 独享 mcache,因此对这部分区域的访问是无锁的

中心缓存 mcentral

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

全局堆缓存 mheap

mheap 是堆内存的抽象,把从OS申请出的内存页组织成 Span 列表。

mcentral 的 Span 不够用时会向 mheap 申请,mheap 的 Span 不够用时会向OS申请。向OS的内存申请是按页来的,然后把申请来的内存页生成 Span 组织起来。

由于这部分空间是所有线程共享的,因此需要加锁访问。

heapArena

每个 heapArena 包含8K个页,即 64MB。当需要分配、回收大对象时,就从 heapArena 中操作

分配原理

小对象

小对象的内存分配,是从最低级的内存单元分配,若没有空闲的内存空间,就向上一级的内存单元申请,直到找到合适的内存单元。

从 mspan 分配对象空间

span 可以按对象大小切成很多份

随着内存的分配,span `中的对象内存块,有些被占用,有些未被占用。

比如上图,整体代表1个 span ,蓝色块代表已被占用内存,绿色块代表未被占用内存。

当分配内存时,快速找到第一个可用的绿色块,并计算出内存地址并返回即可

span 内的所有内存块都被占用时,没有剩余空间继续分配对象时,mcache 会向 mcentral 申请 spanmcache 拿到 span 后继续分配对象。

mcache 向 mcentral 申请 span

mcentralmcache 一样,都是 0~131 这 132 个 span class 级别,但每个级别都保存了2个 span 链表

  • nonempty:这个链表里的 span,所有 span 都至少有1个空闲的对象空间。这些 span 是 mcache 释放 span 时加入到该链表的
  • empty:这个链表里的 span,所有的 span 都不确定里面是否有空闲的对象空间。当一个 span 交给 mcache 的时候,就会加入到 empty 链表

当 mcache 向 mcentral要span时,mcentral会先从nonempty搜索满足条件的span,如果没有找到再从emtpy搜索满足条件的span,然后把找到的span交给mcache。

mcentral 向 mheap 申请 span

mcentral 需要向 mheap 提供需要的内存页数和 span class 级别,然后它优先从 free 中搜索可用的 span ,如果没有找到,会从 scav 中搜索可用的 span,如果还没有找到,它会向OS申请内存

其中

  • free:保存的 span 是空闲并且非垃圾回收的 span
  • scav:保存的是空闲并且已经垃圾回收的 span

如果是垃圾回收导致的 span 释放,span 会被加入到 scav,否则加入到 free。

mheap 向 OS 申请内存

mheap 没有足够的内存时,mheap 会向OS申请额外的内存空间。

大对象

当对象需要申请的内存大于32kb时,会直接从mheap上申请内存

总结

分析 Go 的内存分配机制,总结如下

  • 每次从操作系统申请一大块内存,缓存起来后续使用,以减少系统调用
  • 将申请的大块内存按照特定大小预先切成大、小块,以减少内存碎片
  • 为对象分配内存时,挑选大小合适的块,从中提取空间即可
  • 如果对象销毁,则将对象占用的内存,归还到内存池,以便复用