go-内存管理
Go 的内存是自动管理的,我们可以随意定义变量直接使用,不需要考虑变量背后的内存申请和释放的问题。本文意在搞清楚 Go 在方面帮我们做了什么,使我们不用关心那些复杂内存的问题,还依旧能写出较为高效的程序。
Pool
程序动态申请内存空间,是要使用系统调用的,比如 Linux 系统上是调用 mmap 方法实现的。但对于大型系统服务来说,直接调用 mmap 申请内存,会有一定的代价。比如:
- 内核态与用户态之间的切换浪费系统资源
- 频繁申请小块内存空间容易造成内存碎片
- 为了保证内存访问具有良好的局部性,开发者需要投入大量的精力去做优化,这是一个很重的负担。
解决方案:对象池(缓存)。
假设系统需要频繁动态申请内存来存放一个数据结构,比如 [10]int 。那么我们完全可以在程序启动之初,一次性申请几百甚至上千个 [10]int 。这样完美的解决了上面遇到的问题:
- 不需要频繁申请内存了,而是从对象池里拿,程序不会频繁进入内核态。
- 因为一次性申请一个连续的大空间,对象池会被重复利用,不会出现碎片。
- 程序频繁访问的就是对象池背后的同一块内存空间,局部性良好。
Golang内存管理
Golang 的内存管理本质上就是一个内存池,只不过内部做了很多的优化。比如自动伸缩内存池大小,合理的切割内存块等等。
内存池mheap
Golang的程序在启动时,会一次性从操作系统申请一大块内存作为内存池。这块内存空间会放在一个叫mheap的struct中管理,mheap负责将这一块内存切割成不同的区域,并将其中一部分的内存切割成合适的大小。
关于mheap的几个重要概念:
page:内存页,一块8K大小的内存空间。
span: 一个或者多个连续的page。
sizeclass:空间规格,标记span中的page应该如何使用。
object:对象,用来存储一个变量数据内存空间,一个 span 在初始化时,会被切割成一堆等大的 object 。假设 object 的大小是 16B , span 大小是 8K ,那么就会把 span 中的 page 就会被初始化 8K / 16B = 512 个 object 。所谓内存分配,就是分配一个 object 出去。
示意图:
内部的整体内存布局如下图所示:
- mheap.spans: 存储page和span信息,比如一个span的起始地址,page数目。
- mheap.bitmap: 存储着各个span中对象的标记信息,比如对象是否可回收。
- mheap.arena_start: 将要分配给应用程序使用的空间。
mcentral
用途相同的span会以链表的形式组织在一起。这里的用途用sizeclass来表示。
比如当分配一块大小为 n 的内存时,系统计算 n 应该使用哪种 sizeclass ,然后根据 sizeclass 的值去找到一个可用的 span 来用作分配。其中 sizeclass 一共有 67 种(Go1.5),如图所示:
找到合适的 span 后,会从中取一个 object 返回给上层使用。这些 span 被放在一个叫做 mcentral 的结构中管理。
mheap 将从 OS 那里申请过来的内存初始化成一个大 span (sizeclass=0)。然后根据需要从这个大 span 中切出小 span ,放在 mcentral 中来管理。大 span 由 mheap.freelarge 和 mheap.busylarge 等管理。如果 mcentral 中的 span 不够用了,会从 mheap.freelarge 上再切一块,如果 mheap.freelarge 空间不够,会再次从 OS 那里申请内存重复上述步骤。下面是 mheap 和 mcentral 的数据结构:
1 | GO |
mcache
mcental中有一个lock字段,在高并发场景下必要时用锁来避免冲突。
但是锁是低效的,在高并发的服务中,它会使内存申请成为整个系统的瓶颈;所以在mcentral的前面又加了一层mchache。
每一个 mcache 和每一个处理器(P) 是一一对应的,也就是说每一个 P 都有一个 mcache 成员。 Goroutine 申请内存时,首先从其所在的 P 的 mcache 中分配,如果 mcache 没有可用 span ,再从 mcentral 中获取,并填充到 mcache 中。
从 mcache 上分配内存空间是不需要加锁的,因为在同一时间里,一个 P 只有一个线程在其上面运行,不可能出现竞争。没有了锁的限制,大大加速了内存分配。
其他优化
Tiny对象
面提到的 sizeclass=1 的 span,用来给 <= 8B 的对象使用,所以像 int32 , byte , bool 以及小字符串等常用的微小对象,都会使用 sizeclass=1 的 span,但分配给他们 8B 的空间,大部分是用不上的。并且这些类型使用频率非常高,就会导致出现大量的内部碎片。
所以 Go 尽量不使用 sizeclass=1 的 span, 而是将 < 16B 的对象为统一视为 tiny 对象(tinysize)。配时,从 sizeclass=2 的 span 中获取一个 16B 的 object 用以分配。如果存储的对象小于 16B ,这个空间会被暂时保存起来 (mcache.tiny 字段),下次分配时会复用这个空间,直到这个 object 用完为止。
大对象
如上面所述,最大的 sizeclass 最大只能存放 32K 的对象。如果一次性申请超过 32K 的内存,系统会直接绕过 mcache 和 mcentral,直接从 mheap 上获取,mheap 中有一个 freelarge 字段管理着超大 span。