Go内存管理——内存分配

325 阅读5分钟

内存管理

内存管理的基本要求有两点:

  1. 根据应用程序的请求动态分配内存给程序
  2. 并在不再需要时释放它以供重用

内存管理通常可以分为两类:

  • 手动内存管理:C、C++
  • 自动内存管理:Go、Java….
  • 手动+自动管理:Ada、Modula-3、C++/CLI…

虚拟内存布局

(m)page

go中,单个内存页(page)的大小为8KB

(m)span

一组page构成一个span,span的大小由sizeClass表示,sizeClass共有67种:

// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8
//     4         32        8192      256           0     21.88%         32
//     5         48        8192      170          32     31.52%         16
//     6         64        8192      128           0     23.44%         64
//     7         80        8192      102          32     19.07%         16
//     8         96        8192       85          32     15.95%         32
//     9        112        8192       73          16     13.56%         16
//  ...
//    63      21760       65536        3         256      6.25%        256
//    64      24576       24576        1           0     11.45%       8192
//    65      27264       81920        3         128     10.00%        128
//    66      28672       57344        2           0      4.91%       4096
//    67      32768       32768        1           0     12.50%       8192

表中各列的含义分别为: 类别 、span中存储的对象的最小大小、 span的大小、 span中能否存放的对象数量、span尾部浪费的大小、最大的浪费空间、最小的对齐系数

以表中的第四个跨度类为例,跨度类为 5 的 runtime.mspan 中对象的大小上限为 48 字节、管理 1 个页、最多可以存储 170 个对象。因为内存需要按照页进行管理,所以在尾部会浪费 32 字节的内存,当页中存储的对象都是 33 (32 + 1) 字节时,最多会浪费 31.52% 的资源:

(4833)170+328192=0.31518\frac{(48−33)∗170+32}{8192}=0.31518

image.png 虚拟内存中实际正在使用的内存称为Resident Set(驻留内存)。

页堆(page heap, mheap)

mheap通过将span归类为不同结构进行管理的:

  • arena: 使用arena来管理内存,每个arena管理的大小为64MB

  • mspan:mspan是mheap中管理的内存页的最基本结构。这是一个双向链接列表,其中包含起始页面的地址,span size class和span中的页面数量。像TCMalloc一样,Go将内存页按大小分为67个不同类别,大小从8字节到32KB,如下图所示

    image.png

    每个span存在两个,一个span用于带指针的对象(scan class),一个用于无指针的对象(noscan class)。这在GC期间有帮助,因为noscan类查找活动对象时无需遍历span。

    mspan有3种状态:

    • free
    • in-use
    • manual

    这3种状态之间的转换关系如下:

    • free → in-use / manual : 在GC的任意阶段都有可能发生
    • 在 sweeping (gcphase == _GCoff) 期间,mspan可能从 in-use 转换为 free (由于sweeping) 或 从 manual 转换为 free (由于栈被释放)
    • 在 GC (gcphase != _GCoff) 期间,mspan 无法 从 manual 或 in-use 转化能为 free. 因为并发GC可能会读取指针,然后查找其mspan,mspan的状态必须唯一。
  • mcentral:mcentral将相同大小级别的span归类在一起。

    • 访问时需要互斥锁

    • 每个mcentral包含两个mspanList:

      • empty:双向span链表,包括没有空闲对象的span或缓存mcache中的span。当此处的span被释放时,它将被移至non-empty span链表。
      • non-empty:有空闲对象的span双向链表。当从mcentral请求新的span,mcentral将从该链表中获取span并将其移入empty span链表。’
  • mcache: mcache表示线程缓存,与P绑定,主要用来缓存用户申请的微小对象。

    • 由于与P绑定,每个线程都有一个mcache,因此不需要锁

Go如何分配内存

何时会进行分配内存?

在Go中,包括但不限于以下情况时,会进行内存分配:

  • 调用 new 和 make
  • 创建 maps、slices、匿名函数 字面量
  • 声明变量
  • 将非接口值赋值给接口值
  • 连接非常量字符串
  • 将字符串转换为字节或字符切片,或反过来
  • 将整数转换为字符串
  • 调用 append 函数(扩容时)
  • 向map中添加新的kv(扩容时)

如何分配?

Go运行时会将对象按大小分为: 成微对象、小对象和大对象,根据大小选择不同的分配逻辑。

  • 微对象 (0, 16B) — 先使用微型分配器(不用加锁),再依次尝试线程缓存、中心缓存和堆分配内存;
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
  • 大对象 (32KB, +∞) — 直接在堆上分配内存;

Note: 对象会沿着缓存的层次结构向上分配

Note: 如果堆空间不够用了,则需向操作系统另外申请内存。

参考