Go - 内存分配

347 阅读12分钟

Go - 内存分配

一、介绍一般程序内存分配

在讲Golang的内存分配之前,让我们先来看看一般程序的内存分布情况:

内存区域存储的内存说明
全局区存放全局变量1. 编译的时候以及分配好了 2. 有操作系统管理
栈区存放函数中的基础类类型变量1. 由程序系统向系统申请、由操作系统管理 2. 每个线程由自己的栈区、速度快、适用方便
堆区动态分配的内存, 比如 go 的切片1. 程序运行时动态分配内存大小、频繁分配和释放 2. 内存可以由程序员控制、free或者delete
常量区存放常量数据1. 存放常量字符串内存、程序结束后由系统释放
程序代码区存放程序本身的代码1. 存放程序的二进制代码

二、Go的内存分配思想

go 内置了运行时的编程语言(runtime), 所谓运行时, 就是在程序开始时就申请了一大块的虚拟内存, 由 go 自己进行分配和管理. 用来避免在运行的时候再向操作系统申请内存, 带来性能问题。

go 的内存分配核心思想是:

  • 每次从操作系统申请一大块内存, 由 go 来对内存进行分配和管理, 减少系统调用
  • 内存分配算法采用 google 的 TCMalloc 算法, 把内存切分的很细, 再通过多级进行管理, 降低锁粒度
  • 回收对象内存时,并不是真正的将内存返回给操作系统, 而是放回自己的大块内存中等待复用, 只有闲置过多时才会尝试返回部分内存给操作系统.

三、Go语言内存结构

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

image.png

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

  • arena区域

    Go动态分配的内存都是在这个区域 , arena的大小为512G,为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个 页

  • bitmap区域

    bitmap区域标识arena区域哪些地址保存了对象,不过主要用于GC。 其中一个byte包括8位,用4位标志表示对象是否包含指针、用4位标志GC扫描标记信息。

image.png

  • spans区域

    存放span的指针,每个指针对应一个page,所以span区域的大小为(512GB/8KB)*指针大小8byte = 512M

四、内存管理组件

go 内存管理组件有以下几个:

  • mspan: 内存管理基本单元
  • mcache: 缓存, 每个运行时的 goroutine 都会绑定一个 mcache, mcache 会分配这个 goroutine 运行时需要的内存空间(mspan)
  • mcentral: 为所有 mcache 切分好后备的 mspan, 收集给定大小和登记的所有 span
  • mheap:代表Go程序持有的所有堆空间。还会管理闲置的span,需要时向操作系统申请新内存

4.1 mspan

span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的一页 会划分更小的粒度,而对于大对象比如超过页大小,则通过多页实现。

go 为了解决内存碎片问题. 将内存分为67种, 每种有不同数量的 page, 这每一种就是 mspan.每次分配时, 根据数据的不同, 分配给不同的 mspan. 当某个 mspan 被清理后, 在语言内部将这个标记为已清理, 等待下一次重新使用。


class系列

跟据对象大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小。如下表所示:

// 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%
//    11        160        8192       51          32      9.73%
//    12        176        8192       46          96      9.59%
//    13        192        8192       42         128      9.25%
//    14        208        8192       39          80      8.12%
//    15        224        8192       36         128      8.15%
//    16        240        8192       34          32      6.62%
//    17        256        8192       32           0      5.86%
//    18        288        8192       28         128     12.16%
//    19        320        8192       25         192     11.80%
//    20        352        8192       23          96      9.88%
//    21        384        8192       21         128      9.51%
//    22        416        8192       19         288     10.71%
//    23        448        8192       18         128      8.37%
//    24        480        8192       17          32      6.82%
//    25        512        8192       16           0      6.05%
//    26        576        8192       14         128     12.33%
//    27        640        8192       12         512     15.48%
//    28        704        8192       11         448     13.93%
//    29        768        8192       10         512     13.94%
//    30        896        8192        9         128     15.52%
//    31       1024        8192        8           0     12.40%
//    32       1152        8192        7         128     12.41%
//    33       1280        8192        6         512     15.55%
//    34       1408       16384       11         896     14.00%
//    35       1536        8192        5         512     14.00%
//    36       1792       16384        9         256     15.57%
//    37       2048        8192        4           0     12.45%
//    38       2304       16384        7         256     12.46%
//    39       2688        8192        3         128     15.59%
//    40       3072       24576        8           0     12.47%
//    41       3200       16384        5         384      6.22%
//    42       3456       24576        7         384      8.83%
//    43       4096        8192        2           0     15.60%
//    44       4864       24576        5         256     16.65%
//    45       5376       16384        3         256     10.92%
//    46       6144       24576        4           0     12.48%
//    47       6528       32768        5         128      6.23%
//    48       6784       40960        6         256      4.36%
//    49       6912       49152        7         768      3.37%
//    50       8192        8192        1           0     15.61%
//    51       9472       57344        6         512     14.28%
//    52       9728       49152        5         512      3.64%
//    53      10240       40960        4           0      4.99%
//    54      10880       32768        3         128      6.24%
//    55      12288       24576        2           0     11.45%
//    56      13568       40960        3         256      9.99%
//    57      14336       57344        4           0      5.35%
//    58      16384       16384        1           0     12.49%
//    59      18432       73728        4           0     11.11%
//    60      19072       57344        3         128      3.57%
//    61      20480       40960        2           0      6.87%
//    62      21760       65536        3         256      6.25%
//    63      24576       24576        1           0     11.45%
//    64      27264       81920        3         128     10.00%
//    65      28672       57344        2           0      4.91%
//    66      32768       32768        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大小,超过32K大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。

mspan的数据结构

span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大小,span将一个或多个页拆分成多 个块进行管理。

src/runtime/mheap.go:mspan

type mspan struct {
    next *mspan            //链表前向指针,用于将span链接起来
    prev *mspan            //链表前向指针,用于将span链接起来
    startAddr uintptr      // 起始地址,也即所管理页的地址
    npages    uintptr      // 管理的页数
​
    nelems uintptr         // 块个数,也即有多少个块可供分配
​
    allocBits  *gcBits     //分配位图,每一位代表一个块是否已分配
​
    allocCount  uint16     // 已分配块的个数
    spanclass   spanClass  // class表中的class ID
​
    elemsize    uintptr    // class表中的对象大小,也即块大小
}
​
​

以class 10为例span和管理的内存 image.png

spanclass为10,参照class表可得出 npages=1 , nelems=56 , elemsize为144。

其中startAddr是在span初始 化时就指定了某个页的地址。

allocBits指向一个位图,每位代表一个块是否被分配,本例中有两个块已经被分配, 其allocCount也为2。

next和prev用于将多个span链接起来,这有利于管理多个span。

4.2 mcache

有了管理内存的基本单位span,还要有个数据结构来管理span,这个数据结构叫mcentral,各线程需要内存时从 mcentral管理的span中申请内存。

为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓 存,这个缓存即是cache。

src/runtime/mcache.go:mcache

type mcache struct {
     .....
     tiny uintptr
     tinyoffset uintptr
     tinyAllocs uintptr
     .....
     alloc [67*2]*mspan // 按class分组的mspan列表
     .....
}
​
  • alloc为mspan的指针数组,数组大小为class总数的2倍。
  • 数组中每个元素代表了一种class类型的span列表,每 种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指 针 (这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描)

根据对象是否包含指针,将对象分为noscan和scan两类,其中noscan代表没有指针,而scan则代表有指针,需要 GC进行扫描。


mcache和span的关系 image.png

上图所示,class 0的span数比class1的要多,说明本线程中分配的小对象要多一些。 mchache在初始化时是没有任何span的,在使用过程中会动态的从central中获取并缓存下来,跟据使用情况每种 class的span个数也不相同。

4.3 mcentral

cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务,当某个线程内存不足时会 向central申请,当某个线程释放内存时又会回收进central 。

src/runtime/mcentral.go:mcentral

type mcentral struct {
    lock      mutex     //互斥锁
    spanclass spanClass // span class ID
    nonempty  mSpanList // non-empty 指还有空闲块的span列表
    empty     mSpanList // 指没有空闲块的span列表
​
    nmalloc uint64      // 已累计分配的对象个数
}
​
  • lock: 线程间互斥锁,防止多线程读写冲突
  • spanclass: 每个mcentral管理着一组有相同class的span列表
  • nonempty: 指还有内存可用的span列表
  • empty: 指没有内存可用的span列表
  • nmalloc: 指累计分配的对象个数

image.png

线程从central获取span步骤如下:

  1. 加锁
  2. 从nonempty列表获取一个可用span,并将其从链表中删除
  3. 将取出的span放入empty链表
  4. 将span返回给线程
  5. 解锁
  6. 线程将该span缓存进cache

线程将span归还步骤如下:

  1. 加锁
  2. 将span从empty列表删除
  3. 将span加入noneempty列表
  4. 解锁

mcentral被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。

4.4 mheap

从mcentral数据结构可见,每个mcentral对象只管理特定的class规格的span。

事实上每种class都会对应一个 mcentral,这个mcentral的集合存放于mheap数据结构中。

src/runtime/mheap.go:mheap

type mheap struct {
    lock      mutex
​
    spans []*mspan
​
    bitmap        uintptr     //指向bitmap首地址,bitmap是从高地址向低地址增长的
​
    arena_start uintptr        //指示arena区首地址
    arena_used  uintptr        //指示arena区已使用地址位置
​
    central [67*2]struct {
        mcentral mcentral
        pad      [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
    }
}
  • lock: 互斥锁
  • spans: 指向spans区域,用于映射span和page的关系
  • bitmap:bitmap的起始地址
  • arena_start: arena区域首地址
  • arena_used: 当前arena已使用区域的最大地址
  • central: 每种class对应的两个mcentral

从数据结构可见,mheap管理着全部的内存,事实上Golang就是通过一个mheap类型的全局变量进行内存管理的。

系统预分配的内存分为spans、bitmap、arean三个区域通过mheap管理起来

image.png

五、内存分配的过程

针对待分配对象的大小不同有不同的分配逻辑:

  • (0 , 16B) :且不包含指针的对象 ,mcache上的Tiny分配
  • (0 , 16B) : 包含指针的对象、正常分配
  • [16B , 32KB] : 正常分配
  • (32KB , -) : 大对象直接从mheap上分配

以申请size为n的内存为例,分配步骤如下:

  1. 获取当前线程的私有缓存mcache
  2. 跟据size计算出适合的class的ID
  3. 从mcache的alloc[class]链表中查询可用的span
  4. 如果mcache没有可用的span则从mcentral申请一个新的span加入mcache中
  5. 如果mcentral中也没有可用的span则从mheap中申请一个新的span加入mcentral
  6. 从该span中获取到空闲对象地址并返回

Go语言的内存分配非常复杂,它的一个原则就是能复用的一定要复用。文章从一个比较粗的角度来看Go的内存分配,并没有深入细节。一般而言,了解它的原理到这个程度也可以了。

六、参考链接

图解Go语言内存分配

Golang 内存分配

图解Golang的内存分配