go内存管理
基础概念
Go的内存分配核心思想
Go是内置运行时的编程语言(runtime),像这种内置运行时的编程语言通常会抛弃传统的内存分配方式,改为自己管理。这样可以完成类似预分配、内存池等操作,以避开系统调用带来的性能问题,防止每次分配内存都需要系统调用。
Go的内存分配的核心思想可以分为以下几点:
- 每次从操作系统申请一大块儿的内存,由Go来对这块儿内存做分配,减少系统调用
- 内存分配算法采用Google的
TCMalloc算法
。算法比较复杂,究其原理可自行查阅。其核心思想就是把内存切分的非常的细小,分为多级管理,以降低锁的粒度。 - 回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,才会尝试归还部分内存给操作系统,降低整体开销
内存结构
go在程序启动时会分配一块虚拟内存地址是连续的内存, 结构如下:
这一块内存分为了3个区域, 在X64上大小分别是512M, 16G和512G, 它们的作用如下:
arena
arena区域就是我们通常说的heap, go从heap分配的内存都在这个区域中.为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个页;
bitmap
bitmap区域用于表示arena区域中哪些地址保存了对象, 并且对象中哪些地址包含了指针.
bitmap区域中一个byte(8 bit)对应了arena区域中的四个指针大小的内存, 也就是2 bit对应一个指针大小的内存.所以bitmap区域的大小是 512GB / 指针大小(8 byte) / 4 = 16GB.
bitmap区域中的一个byte对应arena区域的四个指针大小的内存的结构如下,
每一个指针大小的内存都会有两个bit分别表示是否应该继续扫描和是否包含指针:
bitmap中的byte和arena的对应关系从末尾开始, 也就是随着内存分配会向两边扩展:
spans
spans区域用于表示arena区中的某一页(Page)属于哪个span, 什么是span将在下面介绍.
spans区域中一个指针(8 byte)对应了arena区域中的一页(在go中一页=8KB).
所以spans的大小是 512GB / 页大小(8KB) * 指针大小(8 byte) = 512MB.
spans区域的一个指针对应arena区域的一页的结构如下, 和bitmap不一样的是对应关系会从开头开始:
span
span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的一页会划分更小的粒度,而对于大对象比如超过页大小,则通过多页实现。
class
跟据对象大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小。如下表所示:
上表中每列含义如下:
- class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型
- bytes/obj:该class代表对象的字节数
- bytes/span:每个span占用堆的字节数,也即页数*页大小
- objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)
- waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)
上表可见部分大小的对象在分配时会浪费一定的空间.
有人可能会注意到, 上面最大的span的元素大小是32K, 那么分配超过32K的对象会在哪里分配呢?
超过32K的对象称为”大对象”, 分配大对象时, 会直接从heap分配一个特殊的span,
这个特殊的span的类型(class)是0, 只包含了一个大对象, span的大小由对象的大小决定.
特殊的span加上的66个标准的span, 一共组成了67个span类型.
内存管理组件
go的内存管理组件主要有:mspan
、mcache
、mcentral
和mheap
mspan
为内存管理的基础单元,直接存储数据的地方。mcache
:每个运行期的goroutine都会绑定的一个mcache
(具体来讲是绑定的GMP并发模型中的P,所以可以无锁分配mspan
,后续还会说到),mcache
会分配goroutine运行中所需要的内存空间(即mspan
)。mcentral
为所有mcache
切分好后备的mspan
mheap
代表Go程序持有的所有堆空间。还会管理闲置的span,需要时向操作系统申请新内存。
mspan
src/runtime/mheap.go:mspan定义了其数据结构:
span和管理的内存如下图所示:(以class 10为例)
spanclass为10,参照class表可得出npages=1,nelems=56,elemsize为144。其中startAddr是在span初始化时就指定了某个页的地址。allocBits指向一个位图,每位代表一个块是否被分配,本例中有两个块已经被分配,其allocCount也为2。
next和prev用于将多个span链接起来,这有利于管理多个span
mcache
mcache
:每个工作线程都会绑定一个mcache,本地缓存可用的mspan
资源,这样就可以直接给Goroutine分配,因为不存在多个Goroutine竞争的情况,所以不会消耗锁资源。
mcache
的结构体定义:
mcache
用Span Classes
作为索引管理多个用于分配的mspan
,它包含所有规格的mspan
。它是_NumSizeClasses
的2倍,也就是67*2=134
,为什么有一个两倍的关系,前面我们提到过:为了加速之后内存回收的速度,数组里一半的mspan
中分配的对象不包含指针,另一半则包含指针。
mcache
在初始化的时候是没有任何mspan
资源的,在使用过程中会动态地从mcentral
申请,之后会缓存下来。当对象小于等于32KB大小时,使用mcache
的相应规格的mspan
进行分配。
mcentral
mcentral
:为所有mcache
提供切分好的mspan
资源。每个central
保存一种特定大小的全局mspan
列表,包括已分配出去的和未分配出去的。 每个mcentral
对应一种mspan
,而mspan
的种类导致它分割的object
大小不同。当工作线程的mcache
中没有合适(也就是特定大小的)的mspan
时就会从mcentral
获取。
mcentral
被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。结构体定义:
empty
表示这条链表里的mspan
都被分配了object
,或者是已经被cache
取走了的mspan
,这个mspan
就被那个工作线程独占了。而nonempty
则表示有空闲对象的mspan
列表。每个central
结构体都在mheap
中维护。
简单说下mcache
从mcentral
获取和归还mspan
的流程:
- 获取
加锁;
从nonempty
链表找到一个可用的mspan
;并将其从nonempty
链表删除;
将取出的mspan
加入到empty
链表;将mspan
返回给工作线程;
解锁。
- 归还
加锁;
将mspan
从empty
链表删除;
将mspan
加入到nonempty
链表;
解锁。
mheap
mheap
:代表Go程序持有的所有堆空间,Go程序使用一个mheap
的全局对象_mheap
来管理堆内存。
当mcentral
没有空闲的mspan
时,会向mheap
申请。而mheap
没有资源时,会向操作系统申请新内存。mheap
主要用于大对象的内存分配,以及管理未切割的mspan
,用于给mcentral
切割成小对象。
同时我们也看到,mheap
中含有所有规格的mcentral
,所以,当一个mcache
从mcentral
申请mspan
时,只需要在独立的mcentral
中使用锁,并不会影响申请其他规格的mspan
。
mheap
结构体定义:
- lock: 互斥锁
- spans: 指向spans区域,用于映射span和page的关系
- bitmap:bitmap的起始地址
- arena_start: arena区域首地址
- arena_used: 当前arena已使用区域的最大地址
- central: 每种class对应的两个mcentral
从数据结构可见,mheap管理着全部的内存,事实上Golang就是通过一个mheap类型的全局变量进行内存管理的。
mheap内存管理示意图如下:
内存分配过程
Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。
大体上的分配流程:
32KB 的对象,直接从mheap上分配;
<=16B 的对象使用mcache的tiny分配器分配;
(16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
- 如果mcache没有相应规格大小的mspan,则向mcentral申请
- 如果mcentral没有相应规格大小的mspan,则向mheap申请
- 如果mheap中也没有合适大小的mspan,则向操作系统申请
总结
Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。
- Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域
- arena区域按页划分成一个个小块
- span管理一个或多个页
- mcentral管理多个span供线程申请使用
- mcache作为线程私有资源,资源来源于mcentral