内存分配思想
程序中的数据和变量都会被分配到程序所在的虚拟内存中,内存空间包含两个重要区域:栈区(Stack)和堆区(Heap)。
- 函数调用的参数、返回值以及局部变量基本上会被分配到栈上,这部分内存会由编译器进行管理;
- 对于堆区内存,不同编程语言使用不同的方法管理堆区的内存。C++ 等编程语言会由使用者主动申请和释放内存,Go 以及 Java 等编程语言会由使用者和编译器共同管理,堆中的对象由内存分配器分配并由垃圾收集器回收。
Golang 的内存分配器借鉴了 TCMalloc 的思想,尽量减少在多线程模型下,锁的竞争开销,来提高内存分配的效率。
TCMalloc
TCMalloc,其实就是 thread cache malloc 的缩写。看下图大概了解一下 TCMalloc 的思想。

span 这个单词的直译是跨度的意思,在当前的语境下,可以理解为一段连续的内存。下面是一些名词解释:
PageHeap:这是 TCMalloc 中用于管理操作系统页面的部分。它负责较大的分配内存,通常以页(一般为 4KB)为单位。在 PageHeap 当中将多个页组合成 spans,因此 spans 可以是一个或多个连续的页。Span:一个 Span 是由 PageHeap 管理的连续页的集合。因此 Span 可以是单个页面,也可以是多个连续页面的组合。CentralCache:这是 TCMalloc 中的一个中心缓存,用于存放固定大小的内存块,这些内存块在不同大小的 size class 之间分组。中心缓存的作用是给所有的线程的 ThreadCache 分配内存,当某个线程中的内存不够时,向 CentralCache 申请内存时是需要加锁的。size class:为了高效地复用和分配内存,内存块被分为不同大小的 size class。每个 size class 包含大小相同的内存块,以满足不同大小的内存请求。ThreadCache:这是 TCMalloc 中为每个线程分配的本地缓存,用于提供快速的内存分配。当一个线程需要分配内存时,它首先尝试从其 ThreadCache 中获取内存,由于是从本地线程申请内存,所以申请内存时不需要加锁。Application:应用程序,它是内存分配的终端用户,会从 TCMalloc 请求内存来满足其运行需要。
基于 TCMalloc 思想进行内存分配的过程通常如下:
- 当应用程序请求分配内存时,它首先检查本地线程的
ThreadCache中是否有适当大小的内存块。如果有,则直接分配,这是最快的途径。 - 如果线程的
ThreadCache没有可用的内存块,则尝试从CentralCache中获取内存。CentralCache中的内存块是预先按size class分类的。 - 如果
CentralCache中也没有可用的内存块,则从PageHeap中分配一个或多个页面,这会形成一个新的Span,然后将其放入CentralCache。 - 在需要释放内存时,内存块会被放回到对应线程的
ThreadCache。如果ThreadCache已满,它会被移动到CentralCache,并可能最终返回到PageHeap以供将来使用。
这就是基于 TCMalloc 思想进行内存分配的大致流程。总的来说,就是先向操作系统申请一大块内存,然后再对这一大块内存进行瓜分,分别分配给 PageHeap、CentralCache 和 ThreadCache。应用程序需要内存的时候就向上级申请,上级内存不够就向上上级申请,上上级不够就向上上上级申请。由于向本地的线程的 ThreadCache 申请内存时无需加锁,所以这让内存分配的效率有了一定的提升。
内存分配对象的划分
golang 根据对象大小将它们分成微对象、小对象和大对象,并且会根据对象大小的不同选择不同的内存分配方案。不同大小对象的划分规则如下:
- 微对象
(0, 16B):先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存; - 小对象
[16B, 32KB]:依次尝试使用线程缓存、中心缓存和堆分配内存; - 大对象
(32KB, +∞):直接在堆上分配内存;
Go 内存管理组件
Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件,接下来将介绍这几种最重要组件对应的数据结构 mspan、mcache、mcentral 和 mheap。
所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局,每一个处理器(P)都会分配一个线程缓存 mcache 用于处理微对象和小对象的分配,它们会持有内存管理单元 mspan。
内存管理单元:mspan
mspan是 Go 语言内存管理的基本单元。每个 mspan 都对应一个大小等级,总共 67 个等级。mspan 结构体中包含 next 和 prev 两个字段,它们分别指向了前一个和后一个 mspan:
// src\runtime\mheap.gotype mspan struct { next *mspan prev *mspan list *mSpanList ...}type mSpanList struct { first *mspan last *mspan}mspan 串联后会构成如下双向链表,运行时会使用 mSpanList 存储双向链表的头节点和尾节点,并在 mcache 以及 mcentral 中使用。

页
每个 mspan 都管理 npages 个大小为 8KB 的页,这里的页不是操作系统中的内存页,它们是操作系统内存页(一般是 4KB)的整数倍,mspan 会使用下面这些字段来管理内存页的分配和回收:
type mspan struct { startAddr uintptr // 起始地址 npages uintptr // 页数 freeindex uintptr allocBits *gcBits gcmarkBits *gcBits allocCache uint64 ...}startAddr和npages:确定 mspan 管理的多个页所在的内存,每个页的大小都是 8KB。知道了内存的起始地址、每页的大小和数量就能确定 mspan 所管理的一段连续内存所在的区域;freeindex:是一个索引,指示从哪个位置开始查找下一个空闲对象。这个索引可以加速内存分配过程,因为分配器知道无需从头开始检查内存块;allocBits:一个位图,用于表示mspan管理的内存中的对象是否已经被分配。如果相应的位被设置为 1,说明对应的对象已经被分配,如果是 0,则对象空闲;gcmarkBits:也是一个位图,用于垃圾回收过程中标记对象是否可以访问(即是否被 “标记”)。在垃圾回收时,这些位用来决定哪些对象应该被回收;allocCache:allocBits 的补码,可以用于快速查找内存中未被使用的内存;

所以,当程序中的某个对象需要申请内存时,内存分配器会在相应大小的 mspan中根据相应的字段查找空余的空间来分配给这些对象。
跨度类
spanClass 是 mspan 的跨度类,它决定了内存管理单元中存储的对象大小和个数。这么讲可能有点抽象,通俗一点就是:mspan 这个内存管理单元是分不同大小的,有小一点的 mspan,也有大一点的 mspan,为了区分这些不同的 mspan,需要用一个唯一的 ID 去标识不同的 mspan,这个 ID 就是跨度类。
type mspan struct { spanclass spanClass ...}type spanClass uint8Go 语言的内存管理模块中一共包含 67 种跨度类,每一个跨度类都会存储特定大小的对象,并且包含特定数量的页数和对象。67 种跨度类是从 8B 到 32KB,具体的详细信息可以在 go 源码中找到(src\runtime\sizeclasses.go)。下面是一张简化版的表:
下面是各个列的含义:
class:可以认为是跨度类的 ID;bytes/obj:一次对外提供的内存大小。比如程序需要申请一个 40 字节的对象,内存分配器分不会分配 40 字节的内存,而是分配 48 字节的内存。bytes/span:当前的跨度类所对应的 mspan 的大小。objects:当前 mspan 一共能存放的对象的个数。以 class=7 为例,mspan 的大小为 8192,那么 8192 / 80=102.4,也就是当前的 mspan 可以放下 102 个大小为 80 字节的对象。tail waste:mspan 放满后的浪费内存。max waste:最大可能浪费的空间所占百分比。还是以 class=7 为例,如果每个分配的对象都是 65 字节(上一档位的 obj size + 1),那么分配 102 后总共浪费的内存就是:(80 - 65) 102 + 32 = 1562字节,浪费的内存占比就是(1562 / 8192) 100%=19.07%
关于 mspan 的总结:
- mspan 按照大小进行分类共有 67 种,这 67 个 mspan 通过双向链表进行连接。
- 每一种 mspan 都是由一个或多个 8KB 的页组成的一段连续的内存。
- 对于每一种 mspan,都有与之对应的对象(obj)的大小。当应用程序需要申请内存时,根据申请内存的大小来找到对应的 mspan,然后通过 mspan 上记录的一些字段值来进行扫描,到扫描到空闲对象的时候,就把该对象对应的内存空间分配给应用程序。
线程缓存:mcache
mcache是与 Go 协程调度模型 GMP 中的 P(调度器)所绑定,而不是和线程 M 绑定,因为在 Go 调度的 GMP 模型中,真正可运行的线程 M 的数量与 P 的数量一致,即 GOMAXPROCS 个,跟 P 绑定节省了 P 移动到其他 M 上去的 mcahe 的切换开销。每个线程分配一个 mcache 用于处理微对象和小对象的分配。因为是每个线程独有的,所以不需要加锁申请内存。
mcache在刚刚被初始化时是不包含 mspan 的,只有当用户程序申请内存时才会从上一级组件获取新的 mspan 满足内存分配的需求。并且:
- mcache 会持有 tiny 相关字段用于微对象内存分配。
- mcache 会持有 mspan 用于小对象内存分配。
mcache 的数据结构如下:
type mcache struct { tiny uintptr // 申请tiny对象的起始地址 tinyoffset uintptr // 从tiny地址开始的偏移量 tinyAllocs uintptr // tiny对象分配的数量 alloc [numSpanClasses]*mspan // 待分配的mspan列表,通过spanClass作为索引}微分配器只会给非指针类型的对象分配为微内存。tiny,tinyoffset 和 tinyAllocs 是跟 tiny 微对象分配相关的参数;alloc 是待分配的 mspan 列表,不同规格的 mspan 通过 spanClass 值作为索引。
如下是 runtime.mcache 的内存结构,主要包含两部分的内容,Tiny 对象和小对象的分配空间,小对象的内存分配空间是由 136(68 2+1)个 spanClass 规格大小的 mspan 组成的列表。
关于 mcache 的总结:
- 一个 mcache 和一个 P(调度器)进行绑定,当这个 P 与当前 M 线程解绑,并和其它 M 线程进行绑定时,不需要进行 mcache 重新分配,节省了 goroutine 获取本地的 mcache 时的上下文切换所带来的性能开销。
- 每一个 mcache 都包含 tiny 对象和小对象的内存空间。其中 tiny 对象除了要满足对象的大小要小于 16B 字节以外,还要求这个对象不包含指针。
- alloc 列表有 136 个索引,noscan 指针指向的 mspan 是用于分配没有内存引用的对象;scan 指针指向的 mspan 是用于分配有内存引用的对象。
中心缓存:mcentral
mcentral 是内存分配器的中心缓存,与线程缓存不同,访问中心缓存中的内存管理单元需要使用互斥锁。
type mcentral struct { spanclass spanClass partial [2]spanSet full [2]spanSet}-
spanclass 表示这个 mcentral 的类型,不同跨度类的 mspan 对应有不同的 mcentral 管理。Size Class 总共有 0,8B,16B,24B,32B 到 32KB 共 68 种,因此 Go 内存分配器的中央缓存模型 MCentral 总共有 67 种 mcentral(会排除掉 0)。
-
partial 和 full 分别维护空闲的 mspan 集合和已经被使用的 mspan 集合。mcache 向 mcentral 申请资源,当然是从 partial 集合获取。partial 和 full 都是一个[2]spanSet 类型,也就每个 partial 和 full 都各有两个 spanSet 集合,这是为了给 GC 垃圾回收来使用的,其中一个集合是已扫描的,另一个集合是未扫描的。

页堆:mheap
mheap是内存分配的核心结构体,Go 语言程序会将其作为全局变量存储,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。如下是页堆的内存示意图
总结
Go 内存分配器中使用到的多级缓存机制,是程序开发中常用的设计理念,将对象根据大小分为不同规格,通过提高数据局部性和细粒度内存的复用率,能够有效提升不同大小对象的内存分配的整体效率。
附录(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 |
| 10 | 128 | 8192 | 64 | 0 | 11.72% | 128 |
| 11 | 144 | 8192 | 56 | 128 | 11.82% | 16 |
| 12 | 160 | 8192 | 51 | 32 | 9.73% | 32 |
| 13 | 176 | 8192 | 46 | 96 | 9.59% | 16 |
| 14 | 192 | 8192 | 42 | 128 | 9.25% | 64 |
| 15 | 208 | 8192 | 39 | 80 | 8.12% | 16 |
| 16 | 224 | 8192 | 36 | 128 | 8.15% | 32 |
| 17 | 240 | 8192 | 34 | 32 | 6.62% | 16 |
| 18 | 256 | 8192 | 32 | 0 | 5.86% | 256 |
| 19 | 288 | 8192 | 28 | 128 | 12.16% | 32 |
| 20 | 320 | 8192 | 25 | 192 | 11.80% | 64 |
| 21 | 352 | 8192 | 23 | 96 | 9.88% | 32 |
| 22 | 384 | 8192 | 21 | 128 | 9.51% | 128 |
| 23 | 416 | 8192 | 19 | 288 | 10.71% | 32 |
| 24 | 448 | 8192 | 18 | 128 | 8.37% | 64 |
| 25 | 480 | 8192 | 17 | 32 | 6.82% | 32 |
| 26 | 512 | 8192 | 16 | 0 | 6.05% | 512 |
| 27 | 576 | 8192 | 14 | 128 | 12.33% | 64 |
| 28 | 640 | 8192 | 12 | 512 | 15.48% | 128 |
| 29 | 704 | 8192 | 11 | 448 | 13.93% | 64 |
| 30 | 768 | 8192 | 10 | 512 | 13.94% | 256 |
| 31 | 896 | 8192 | 9 | 128 | 15.52% | 128 |
| 32 | 1024 | 8192 | 8 | 0 | 12.40% | 1024 |
| 33 | 1152 | 8192 | 7 | 128 | 12.41% | 128 |
| 34 | 1280 | 8192 | 6 | 512 | 15.55% | 256 |
| 35 | 1408 | 16384 | 11 | 896 | 14.00% | 128 |
| 36 | 1536 | 8192 | 5 | 512 | 14.00% | 512 |
| 37 | 1792 | 16384 | 9 | 256 | 15.57% | 256 |
| 38 | 2048 | 8192 | 4 | 0 | 12.45% | 2048 |
| 39 | 2304 | 16384 | 7 | 256 | 12.46% | 256 |
| 40 | 2688 | 8192 | 3 | 128 | 15.59% | 128 |
| 41 | 3072 | 24576 | 8 | 0 | 12.47% | 1024 |
| 42 | 3200 | 16384 | 5 | 384 | 6.22% | 128 |
| 43 | 3456 | 24576 | 7 | 384 | 8.83% | 128 |
| 44 | 4096 | 8192 | 2 | 0 | 15.60% | 4096 |
| 45 | 4864 | 24576 | 5 | 256 | 16.65% | 256 |
| 46 | 5376 | 16384 | 3 | 256 | 10.92% | 256 |
| 47 | 6144 | 24576 | 4 | 0 | 12.48% | 2048 |
| 48 | 6528 | 32768 | 5 | 128 | 6.23% | 128 |
| 49 | 6784 | 40960 | 6 | 256 | 4.36% | 128 |
| 50 | 6912 | 49152 | 7 | 768 | 3.37% | 256 |
| 51 | 8192 | 8192 | 1 | 0 | 15.61% | 8192 |
| 52 | 9472 | 57344 | 6 | 512 | 14.28% | 256 |
| 53 | 9728 | 49152 | 5 | 512 | 3.64% | 512 |
| 54 | 10240 | 40960 | 4 | 0 | 4.99% | 2048 |
| 55 | 10880 | 32768 | 3 | 128 | 6.24% | 128 |
| 56 | 12288 | 24576 | 2 | 0 | 11.45% | 4096 |
| 57 | 13568 | 40960 | 3 | 256 | 9.99% | 256 |
| 58 | 14336 | 57344 | 4 | 0 | 5.35% | 2048 |
| 59 | 16384 | 16384 | 1 | 0 | 12.49% | 8192 |
| 60 | 18432 | 73728 | 4 | 0 | 11.11% | 2048 |
| 61 | 19072 | 57344 | 3 | 128 | 3.57% | 128 |
| 62 | 20480 | 40960 | 2 | 0 | 6.87% | 4096 |
| 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 |