Golang内存分配器的核心设计思想是:多级内存分配模块,减少内存分配时锁的使用与系统调用;多尺度内存单元,减少内存分配产生碎片。掌握golang内存分配机制可以更好的理解垃圾回收设计原理。

golang的内存管理分为栈内存管理和堆内存管理。栈上内存由编译器管理,堆上内存由程序管理,在运行期间申请和释放(垃圾回收)。至于一个变量分配在堆上还是栈上,与语法无关,主要依靠golang的逃逸分析(见另外一篇文章《如何判断golang变量是分配在栈(stack)上还是堆(heap)上?》)。

栈与堆相比劣势:一是栈空间较小(8M或10M),不适合存放空间占用大的对象;二是函数内的局部变量离开函数体会被自动弹出栈,离开函数作用域依然存活的指针也不适合栈存放。

栈与堆相比优势:一是管理简单(由编译器完成),二是分配和释放速度快(无垃圾回收过程),三是栈上内存有很好的局部性(堆上2块数据可能分布在不同的页上)。

本文主要介绍golang堆上内存管理。

一、多层次、多尺度的内存分配架构

golang的内存分配器借鉴了TCMalloc现代分配器的设计思想:一次性或者提前分配多级内存模块,减少内存分配时锁的使用和与操作系统的沟通;多尺度内存单元,减少内存分配产生碎片

mspan是堆上内存管理的基本单元,由一连串的页构成(8kb)。golang根据内存大小将mspan分成了67个等级,每个mspan被切分成了很多小的对象,用于不对尺度 的对象分配。

每个线程有一个独立的堆内存mcache,在mcache上申请内存不需要加锁,每个mache有67种尺度内存单元,每个尺度的内存又由2个mspan构成,分别存储指针类型和非指针类型。当程序在堆上创建对象时,分配器会根据对象的大小(小于32kb)和类型(是否为指针)从67*2种mspan中分配内存。

如果mache内存不足,会从所有线程共享的缓存mcentral中申请内存。一共有67*2种mcentral, mcache中空间不足的mspan会从对应的mcentral申请内存,申请内存时需要加锁,但是加锁的概率变成了1/(67*2),申请效率很高。为了进一步提升内存分配效率,每一个mcentral由2个链表构成,一个存储已经分配给mcache的mspan,一个存放未被占用或者已经被mcache释放的mspan。

freescav

二、核心数据结构

mHeap

mheap就是上图中间一行。

heapArena

Golang 的堆由很多个 arena 组成,每个 arena 在 64 位机器上是 64MB,且起始地址与 arena 的大小对齐,
所有的 arena 覆盖了整个 Golang 堆的地址空间。

  • bitmap:是一个2MB个byte数组来标记这个heap area 64M 内存的使用情况,bitmap位图主要为GC标记数组,用2bits标记8(PtrSize) 个byte的使用情况。之所以用2个bits,一是标记对应地址中是否存在对象,另外是标记此对象是否被gc标记过。一个功能一个bit位,所以, heap bitmaps用两个bit位;
  • spans:是一个8192(pagesPerArena)大小的指针数组,每个mspan是8KB;
  • pageInUse:是一个位图,使用1024 * 8 bit来标记 8192个页(8192*8KB = 64MB)中哪些页正在使用中;
  • pageMarks:标记页,与GC相关。

arenaHint

arenaHint结构比较简单,是 arenaHint 链表的节点结构,保存了arena 的起始地址、是否为最后一个 arena,以及下一个 arenaHint 指针。

mspan

mspanmspan

这里举一个例子:32byte的span,span占用一个页,所以总共有256个slot:

  1. 这里表示slot大小为32byte的span, 上一次gc之后, 前8个slot使用如上.
  2. freeindex表示 <该位置的都被分配了, >=该位置的可能被分配, 也可能没有. 配合allocCache来寻找. 每次分配后, freeindex设置为分配的slot+1.
  3. allocBits表示上一次GC之后哪一些slot被使用了. 0未使用或释放, 1已分配.
  4. allocCache表示从freeindex开始的64个slot的分配情况, 1为未分配, 0为分配. 使 用ctz(Count Trailing Zeros指令)来找到第一个非0位. 使用完了就从allocBits加载, 取 反.
  5. 每次gc完之后, sweep阶段, 将allocBits设置为gcmarkBits.

mcentral

mcache

三、总结

golang内存管理各组件的关系如下图所示,其中heapArena、span、page从不同的粒度管理内存。mcache、mcentral、mheap从不同层次管理管理内存。golang的内存分配机制是配合垃圾回收设计的,垃圾回收原理我将在后面的文章里介绍。

通过分析golang的内存分配机制,与其他语言的内存分配器比较,可以得出一些通用的内存管理模式:

  1. 每次从操作系统申请一大块内存,以减少系统调用;
  2. 将申请的大块内存按照特定大小预先切成小块,构成链表;
  3. 为对象分配内存时,从大小合适的链表中提取一块即可;
  4. 如果对象销毁,则将对象占用的内存,归还到原链表,以便复用;
  5. 如果限制内存过多,则尝试归还部分给操作系统,降低整体开销。

参考文献