内存管理
内存管理的基本要求有两点:
- 根据应用程序的请求动态分配内存给程序
- 并在不再需要时释放它以供重用
内存管理通常可以分为两类:
- 手动内存管理: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% 的资源:
虚拟内存中实际正在使用的内存称为Resident Set(驻留内存)。
页堆(page heap, mheap)
mheap通过将span归类为不同结构进行管理的:
-
arena: 使用arena来管理内存,每个arena管理的大小为64MB
-
mspan:mspan是mheap中管理的内存页的最基本结构。这是一个双向链接列表,其中包含起始页面的地址,span size class和span中的页面数量。像TCMalloc一样,Go将内存页按大小分为67个不同类别,大小从8字节到32KB,如下图所示
每个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: 如果堆空间不够用了,则需向操作系统另外申请内存。