深入 Go 底层原理(五):内存分配机制
1. 引言
Go 语言的性能不仅得益于其高效的并发调度,还得益于其精心设计的内存分配器。Go 的内存分配器借鉴了 Google 自家的 TCMalloc (Thread-Caching Malloc) 思想,旨在实现高并发场景下的快速、低锁竞争的内存分配。
本文将深入 Go 的内存管理,揭示其分层式的内存分配架构。
2. Go 内存管理的宏观视图
Go 程序在启动时,会向操作系统申请一大块内存,并自行进行管理,避免了频繁的系统调用。这块内存被组织成一个多层级的结构,以优化不同大小对象的分配效率。
Go 的内存管理可以分为两个主要部分:
栈 (Stack) 分配: 用于管理 Goroutine 的函数调用栈。栈内存由 Goroutine 自行管理,分配和回收都非常快(通过移动栈指针),且不涉及 GC。
堆 (Heap) 分配: 用于管理程序动态创建的对象,是本文的重点。堆内存由 Go 的内存分配器和垃圾回收器共同管理。
3. 堆内存分配的核心组件
Go 的堆内存分配器是一个分层结构,主要由 mheap
, mcentral
, mcache
和 mspan
构成。
mspan
(Memory Span)定义:是内存管理的基本单位。一个
mspan
是由runtime
管理的一块连续的、页对齐的内存(Go 中一页为 8KB)。属性:每个
mspan
都被划分为特定大小规格 (Size Class) 的小块,用于存放相同大小的对象。例如,一个mspan
可能专门用于存放 16 字节的对象。Go 预定义了约 70 个不同的 Size Class。
mcache
(Memory Cache)定义:一个与 P (Processor) 绑定的、线程私有的内存缓存。每个 P 都有一个自己的
mcache
。职责:为当前 P 上的 Goroutine 提供无锁的、快速的小对象内存分配。
mcache
中包含一个mspan
数组,每个mspan
对应一种 Size Class。特点:因为
mcache
是 P 私有的,所以当 Goroutine 需要分配小对象时,可以直接从对应的mcache
中获取,无需加锁,效率极高。
mcentral
(Central Memory)定义:一个全局的、所有 P 共享的
mspan
缓存。职责:为所有
mcache
提供mspan
资源。mcentral
按 Size Class 对mspan
进行分组管理,每个 Size Class 都有一个mspan
链表。特点:当一个
mcache
中的某个 Size Class 的mspan
被用完时,它会向mcentral
申请一个新的mspan
。这个过程需要加锁。反之,当mcache
的mspan
有大量空闲时,也会归还给mcentral
。
mheap
(Memory Heap)定义:堆内存的最高管理者,持有从操作系统申请来的所有内存。
职责:管理大量的
mspan
。当mcentral
缺少mspan
时,会向mheap
申请。mheap
会从其管理的页中划分出一块,组成一个新的mspan
交给mcentral
。特点:如果
mheap
自身内存不足,它会通过系统调用(如mmap
)向操作系统申请更多内存(通常以 Arena 为单位,64MB on 64-bit systems)。
4. 内存分配流程
根据对象的大小,分配流程有所不同:
小对象分配 (<= 32KB)
Goroutine 请求分配内存。
计算对象所需的大小规格 (Size Class)。
从当前 P 绑定的
mcache
中,找到对应 Size Class 的mspan
链表。从
mspan
中获取一个空闲的小块内存。此过程无锁。如果
mcache
的mspan
没有空闲块,mcache
会向mcentral
申请一个新的、可用的mspan
。此过程需要加锁。如果
mcentral
也没有可用的mspan
,它会向mheap
申请。如果
mheap
也没有,它会向操作系统申请新内存。
大对象分配 (> 32KB)
对于大对象,分配器会绕过
mcache
和mcentral
,直接由mheap
进行分配。mheap
会寻找能容纳该对象的最佳mspan
。
5. 总结
Go 的内存分配器通过 mcache
-mcentral
-mheap
的三级架构,实现了高效的内存管理。
mcache
:通过 P 私有缓存,实现了小对象的无锁快速分配,这是其高性能的关键。mcentral
:作为中间协调者,平衡了所有mcache
的资源需求。mheap
:作为最终的资源来源,统一管理所有堆内存。
这种设计,在保证高并发性能的同时,也有效地减少了内存碎片。