## 设计原理

### 内存分配方式

- 线性分配
- 空闲链表分配

#### 线性分配方式

- 线性分配（Bump Allocator）是一种高效的内存分配方法。只需要在内存中维护一个指向内存特定位置的指针，如果用户程序向分配器申请内存，分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置，即移动下图中的指针：

![bump-allocator](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281025325.png)

- 线性分配器实现带来较快的执行速度以及较低的实现复杂度，但是线性分配器无法在内存被释放时重用内存。如下图所示，如果已经分配的内存被回收，线性分配器无法重新利用红色的内存：

![bump-allocator-reclaim-memory](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281026235.png)

- 需要与垃圾分配算法配合使用，例如：标记压缩（Mark-Compact）、复制回收（Copying GC）和分代回收（Generational GC）等算法，它们可以通过拷贝的方式整理存活对象的碎片，将空闲内存定期合并，就能利用线性分配器的效率提升内存分配器的性能



#### 空闲链表分配方式

- 空闲链表分配器（Free-List Allocator）可以重用已经被释放的内存，它在内部会维护一个类似链表的数据结构。当用户程序申请内存时，空闲链表分配器会依次遍历空闲的内存块，找到足够大的内存，然后申请新的资源并修改链表：

![free-list-allocator](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281033047.png)

- 不同的内存块通过指针构成了链表，使用这种方式的分配器可以重新利用回收的资源，分配内存时需要遍历链表，所以它的时间复杂度是 𝑂(𝑛)。空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择，最常见的是以下四种：

  - 首次适应（First-Fit）— 从链表头开始遍历，选择第一个大小大于申请内存的内存块；

  - 循环首次适应（Next-Fit）— 从上次遍历的结束位置开始遍历，选择第一个大小大于申请内存的内存块；

  - 最优适应（Best-Fit）— 从链表头遍历整个链表，选择最合适的内存块；

  - 隔离适应（Segregated-Fit）— 将内存分割成多个链表，每个链表中的内存块大小相同，申请内存时先找到满足条件的链表，再从链表中选择合适的内存块；隔离适应策略如下

![segregated-list](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281100525.png)

- 隔离适应策略会将内存分割成由 4、8、16、32 字节的内存块组成的链表，当我们向内存分配器申请 8 字节的内存时，它会在上图中找到满足条件的空闲内存块并返回。隔离适应的分配策略减少了需要遍历的内存块数量，提高了内存分配的效率。

### 分级分配

- Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配，它的核心理念是使用多级缓存将对象根据大小分类，并按照类别实施不同的分配策略

##### 对象大小

- Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑，运行时根据对象的大小将对象分成微对象、小对象和大对象三种：

|  类别  |     大小      |
| :----: | :-----------: |
| 微对象 |  `(0, 16B)`   |
| 小对象 | `[16B, 32KB]` |
| 大对象 | `(32KB, +∞)`  |

##### 多级缓存

- 内存分配器不仅会区别对待大小不同的对象，还会将内存分成不同的级别分别管理，TCMalloc 和 Go 运行时分配器都会引入线程缓存（Thread Cache）、中心缓存（Central Cache）和页堆（Page Heap）三个组件分级管理内存：

![image-20240628111504024](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281115082.png)



#### 虚拟内存布局

##### Go 1.10及以前连续分配

- 启动时会初始化整片虚拟内存区域，如下所示的三个区域 `spans`、`bitmap` 和 `arena` 分别预留了 512MB、16GB 以及 512GB 的内存空间，这些内存并不是真正存在的物理内存，而是虚拟内存：

![heap-before-go-1-10](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281117161.png)

- `spans` 区域存储了指向内存管理单元 [`runtime.mspan`](https://draveness.me/golang/tree/runtime.mspan) 的指针，每个内存单元会管理几页的内存空间，每页大小为 8KB；
- `bitmap` 用于标识 `arena` 区域中的那些地址保存了对象，位图中的每个字节都会表示堆区中的 32 字节是否空闲；
- `arena` 区域是真正的堆区，运行时会将 8KB 看做一页，这些内存页中存储了所有在堆上初始化的对象；

- 对于任意一个地址，都可以根据 `arena` 的基地址计算该地址所在的页数并通过 `spans` 数组获得管理该片内存的管理单元 [`runtime.mspan`](https://draveness.me/golang/tree/runtime.mspan)，`spans` 数组中多个连续的位置可能对应同一个 [`runtime.mspan`](https://draveness.me/golang/tree/runtime.mspan) 结构。

##### Go 1.11以后稀疏分配

- 稀疏的内存布局能移除堆大小的上限

![heap-after-go-1-11](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281123301.png)

### 地址空间

- Go 语言的运行时构建了操作系统的内存管理抽象层，该抽象层将运行时管理的地址空间分成以下四种状态[8](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fn:8)：

  |    状态    |                             解释                             |
  | :--------: | :----------------------------------------------------------: |
  |   `None`   |         内存没有被保留或者映射，是地址空间的默认状态         |
  | `Reserved` |        运行时持有该地址空间，但是访问该内存会导致错误        |
  | `Prepared` | 内存被保留，一般没有对应的物理内存访问，该片内存的行为是未定义的可以快速转换到 `Ready` 状态 |
  |  `Ready`   |                        可以被安全访问                        |

![memory-regions-states-and-transitions](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281149871.png)

运行时中包含多个操作系统实现的状态转换方法，所有的实现都包含在以 `mem_` 开头的文件中，本节将介绍 Linux 操作系统对上图中方法的实现：

- [`runtime.sysAlloc`](https://draveness.me/golang/tree/runtime.sysAlloc) 会从操作系统中获取一大块可用的内存空间，可能为几百 KB 或者几 MB；
- [`runtime.sysFree`](https://draveness.me/golang/tree/runtime.sysFree) 会在程序发生内存不足（Out-of Memory，OOM）时调用并无条件地返回内存；
- [`runtime.sysReserve`](https://draveness.me/golang/tree/runtime.sysReserve) 会保留操作系统中的一片内存区域，访问这片内存会触发异常；
- [`runtime.sysMap`](https://draveness.me/golang/tree/runtime.sysMap) 保证内存区域可以快速转换至就绪状态；
- [`runtime.sysUsed`](https://draveness.me/golang/tree/runtime.sysUsed) 通知操作系统应用程序需要使用该内存区域，保证内存区域可以安全访问；
- [`runtime.sysUnused`](https://draveness.me/golang/tree/runtime.sysUnused) 通知操作系统虚拟内存对应的物理内存已经不再需要，可以重用物理内存；
- [`runtime.sysFault`](https://draveness.me/golang/tree/runtime.sysFault) 将内存区域转换成保留状态，主要用于运行时的调试；

运行时使用 Linux 提供的 `mmap`、`munmap` 和 `madvise` 等系统调用实现了操作系统的内存管理抽象层，抹平了不同操作系统的差异，为运行时提供了更加方便的接口，除了 Linux 之外，运行时还实现了 BSD、Darwin、Plan9 以及 Windows 等平台上抽象层



## 内存管理组件

- 内存管理单元 runtime.mspan
- 线程缓存 runtime.mcache
- 中心缓存 runtime.mcentral
- 页堆 runtime.mheap

### Go内存布局

![go-memory-layout](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/halo2/202406281152718.png)

- 每一个处理器都会分配一个线程缓存 [`runtime.mcache`](https://draveness.me/golang/tree/runtime.mcache) 用于处理微对象和小对象的分配，它们会持有内存管理单元 [`runtime.mspan`](https://draveness.me/golang/tree/runtime.mspan)

- 每个类型的内存管理单元都会管理特定大小的对象，当内存管理单元中不存在空闲对象时，它们会从 [`runtime.mheap`](https://draveness.me/golang/tree/runtime.mheap) 持有的 134 个中心缓存 [`runtime.mcentral`](https://draveness.me/golang/tree/runtime.mcentral) 中获取新的内存单元，中心缓存属于全局的堆结构体 [`runtime.mheap`](https://draveness.me/golang/tree/runtime.mheap)，它会从操作系统中申请内存
- 在 amd64 的 Linux 操作系统上，[`runtime.mheap`](https://draveness.me/golang/tree/runtime.mheap) 会持有 4,194,304 [`runtime.heapArena`](https://draveness.me/golang/tree/runtime.heapArena)，每个 [`runtime.heapArena`](https://draveness.me/golang/tree/runtime.heapArena) 都会管理 64MB 的内存，单个 Go 语言程序的内存上限也就是 256TB。