1. 基础
1.1 进程的虚拟内存
程序运行进程的总大小可以超过实际可用的物理内存的大小。每个进程都可以有自己独立的虚拟地址空间。然后通过CPU和MMU把虚拟内存地址转换为实际物理地址。

最高位的1GB是linux内核空间,用户代码不能写,否则触发段错误。下面的3GB是进程使用的内存。
- Kernel space:linux内核空间内存
- Stack:进程栈空间,程序运行时使用。它向下增长,系统自动管理
- Memory Mapping Segment:内存映射区,通过mmap系统调用,将文件映射到进程的地址空间,或者匿名映射。
- Heap:堆空间。这个就是程序里动态分配的空间。linux下使用malloc调用扩展(用brk/sbrk扩展内存空间),free函数释放(也就是缩减内存空间)
- BSS段:包含未初始化的静态变量和全局变量
- Data段:代码里已初始化的静态变量、全局变量
- Text段:代码段,进程的可执行文件
1.2 虚拟内存大小
操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示:

32
位系统的内核空间占用1G
,位于最高处,剩下的3G
是用户空间;64
位系统的内核空间和用户空间都是128T
,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
1.3 golang内存逃逸分析
C/C++,我们使用 malloc 或者 new 申请的变量会被分配到堆上。但是 Golang 并不是这样,虽然 Golang 语言里面也有 new。Golang 编译器决定变量应该分配到什么地方时会进行逃逸分析。下面是一个简单的例子。
1 | package main |
将上面文件保存为 main.go,执行下面命令
1 | $ go run -gcflags '-m -l' main.go |
上面的意思是 foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了栈上。
如何得知变量是分配在栈(stack)上还是堆(heap)上?准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。
2. 内存管理模型
2.1 TCMalloc算法
Golang运行时的内存分配算法主要源自 Google 为 C 语言开发的 TCMalloc 算法,全称Thread-Caching Malloc。
核心思想就是把内存分为多级管理,从而降低锁的粒度。每个 P 都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
2.2 内存管理单元mspan
Go默认采用8192B(8KB)大小的页,页的粒度保持为8KB。
但是有的变量很小就是数字,有的却是一个复杂的结构体,所以基于TCMalloc模型的Go还将内存页分为67个不同大小级别,从8字节到32KB分了67 种( 8 byte, 16 byte….32KB)。

mspan
:Go中内存管理的基本单元,是一个双向链表对象,其中包含页面的起始地址,它具有的页面的span类以及它包含的页面数。
1 | // path: /usr/local/go/src/runtime/mheap.go |
2.3 mcache (GMP的 P 绑定)
我们知道每个 Gorontine 的运行都是绑定到一个 P 上面,mcache 是每个 P 的 cache。这么做的好处是分配内存时不需要加锁。
mcache 可以为golang中每个 Processor(GMP模型的P) 提供内存cache使用,每一个mcache的组成单位也是mspan。

1 | // Per-thread (in Go, per-P) cache for small objects. |
解释:
1 | alloc [_NumSizeClasses]*mspan |
2.4 mcentral (线程共享)
mcentral
被所有的工作线程共同享有,存在多个goroutine
竞争的情况,因此从mcentral
获取资源时需要加锁。
1 | type mcentral struct { |
解释
1 | sizeclass: 0 ~ _NumSizeClasses 之间的一个值。 |
mcentral
里维护着两个双向链表,nonempty表示链表里还有空闲的mspan
待分配。empty表示这条链表里的mspan
都被分配了object
。
mcache
从mcentral
获取和归还mspan
的流程:
- 获取 加锁;从
nonempty
链表找到一个可用的mspan
;并将其从nonempty
链表删除;将取出的mspan
加入到empty
链表;将mspan
返回给工作线程;解锁。 - 归还 加锁;将
mspan
从empty
链表删除;将mspan
加入到nonempty
链表;解锁。
2.5 mheap(大内存)
mheap负责大内存的分配。当mcentral内存不够时,可以向mheap申请。那mheap没有内存资源呢?跟tcmalloc一样,向OS操作系统申请。
还有,大于32KB的内存,也是直接向mheap申请。

1 | type mheap struct { |
mheap
里的arena
区域是真正的堆区,运行时会将 8KB
看做一页,这些内存页中存储了所有在堆上初始化的对象。
3. 分配机制
Go内存管理的基本单元是mspan,每种mspan可以分配特定大小的object。
mcache,mcentral,mheap是 Go 内存管理的三大组件, mcache 管理线程在本地缓存的 mspan;mcentral 管理全局的 mspan供所有线程使用;mheap管理 Go的所有动态分配内存。
3.1 分配策略
Go在程序启动时,会向操作系统申请一大块内存,由
mheap
结构全局管理。object size < 16 byte,使用 mcache 的小对象分配器 tiny 直接分配。
object size > 16 byte && size <=32K byte 时,先使用 mcache 中对应的 size class 分配。
object size > 32K,则使用 mheap 直接分配。
如果 mcache 对应的 size class 的 span 已经没有可用的块,则向 mcentral 请求。
如果 mcentral 也没有可用的块,则向 mheap 申请。
如果 mheap 也没有合适的 span,则想操作系统申请。

3.2 头脑风暴
- 首先是进程虚拟内存和物理内存映射,用的都是虚拟地址。
- Go借鉴的TCMalloc算法,把内存多级管理,mheap,mcentral,mcache(对应GMP的P)。基本单位是mspan。
- mcache不需要加锁,往上层申请内存需要加锁。
- 小于16字节B,mcache,大于32KB,mheap。中间大小的化从mache,mcenter,mheap逐级向上申请。