125 lines
9.2 KiB
Markdown
125 lines
9.2 KiB
Markdown
|
## 设计原理
|
|||
|
|
|||
|
### 内存分配方式
|
|||
|
|
|||
|
- 线性分配
|
|||
|
- 空闲链表分配
|
|||
|
|
|||
|
#### 线性分配方式
|
|||
|
|
|||
|
- 线性分配(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。
|