go堆内存管理 | 青训营笔记

200 阅读6分钟

这是我参与「第五届青训营」笔记创作活动的第十一天

go堆内存管理

任务

  1. 理解堆内存基本结构
  1. 理解负责内存管理的主要数据结构
  1. 理解mcentral和mcache的工作模式
  1. 理解heapArea和mspan中这些重要的位图标记

堆内存基本结构

image-20230211185152821

Go语言的runtime将Go内存空间划分成了一个一个arena,arena区域的起始位置被定义为常量arenaBaseOffset。

在amd64架构的Linux环境下,每个arena的大小是64M

每个包含8192个page,每个page的大小就是8KB。

因为程序运行起来所分配的内存有大有小,而分散的大小不一的碎片化内存,一方面会降低内存使用率,另一方面要找到大小合适的内存块的代价因碎片化而增加。为降低碎片化内存给程序性能造成的不良影响,go语言的堆分配采用了与tcmalloc内存分配器类似的算法。

简单来说,按照一组预置的大小规格把内存页划分为块,然后把不同规格的内存块放入到对应的空闲链表中。

程序申请内存时,分配器会先根据要申请的内存大小,找到最匹配的规格,然后从对应空闲链表中分配一个内存块。

image-20230211190326475

go1.16 runtime 包给出了67种预置的大小规格,最小8B,最大32KB

堆内存管理数据结构

堆内存之外,有很多管理堆内存的数据结构

  • mheap:用来管理整个堆内存
  • 一个heaparena对应一个arena
  • 一个span对应一个mspan

中文名称对应:

mheap:页堆

runtime.mcentral:中心缓存

runtime.mspan:内存管理单元

runtime.mcache:线程缓存(线程指的是内核线程,mcache存在于P)

runtime.heapArena:(不知道)

mcentral

mheap是用来管理整个堆内存,是最上层的数据结构。

mheap.central是全局的mspan管理中心

mheap中有mheap.central它是长度为136的数组,数组元素是一个mcentral结构,加上一个padding

type mcentral struct {
  spanclass spanClass
  partial  [2]spanSet
  full     [2]spanSet
}

image-20230211192446349

spanclass

一个mcentral就对应一种mspan规格类型,记录在spanclass当中

spanclass的高7位用来标记内存块大小规格编号,runtime提供的预置规格对应编号1到67,编号0留出来,对应大于32KB的大块内存

就一共68种spanclass

然后每一种规格会按照是否需要GC扫描,进一步区分开

spanclass的最低位用来标识是否需要GC扫描,包含指针的需要GC扫描,归位scannable这一类,不含指针的归位noscan这一类

partial full

每个mcentral都会管理某个spanclass的内存管理单元,它会同时持有两个 runtime.spanSet,分别存储包含空闲对象(partial)和不包含空闲对象(full)的内存管理单元。

在mcentral当中,会将已用尽的和未用尽的mspan进行管理

每一种会放在两个并发安全的Set当中,一个是已清扫的,一个是未清扫的

mcache

全局mspan管理中心方便取用各种类型的mspan

但是为了保障多个P之间并发安全,免不了频繁加锁

为了降低多个P之间的竞争性,go语言为每个P都分配了一个本地对象缓存(runtime.mcache),使用mcache就不用加锁去分配内存了

image-20230211195528540

mcache tiny主要用来缓存用户程序申请的微小对象、noscan类型的内存

mcache还有一个长度为138的*mspan类型的数组

当前P需要用到特定类型的mspan时,先去本地缓存找到对应的mspan,如果没有或者用完了,就去mcentral里面获取一个放到当前P的mcache里,把已用完的归还到对应的mcentral里面的full Set集合里

heapArena

heapArena是arena的管理中心

每一个 runtime.heapArena都会管理 64MB 的内存空间(除来32位到系统 和windows系统)

type heapArena struct{
  bitmap [heapArenaBitmapBytesbyte
  spans [pagesPer Arena)*mspan
  pageInUse [pagesPerArena 8]uint8
  pageMarks [pagesPerArena/8Juint8
  pageSpecials [pagesPerArena/8]uint8
  checkmarks *checkmarksMap
  zeroedBase uintptr
}

这里存储着arena的元数据,里面有一群位图标记

  • bitmap位图用一位来标记这个arena中,一个指针大小的内存单元,到底是指针还是标量;在用一位标记这块内存空间的后续单元,是否包含指针。

    而且为了便于操作,bitmap中用1字节标记arena中4个指针大小的内存空间。

    低4位用来标记指针/标量,高4位用来标记扫描/终止。

    image-20230211202718412

    例如在arena起始处分配一个slice,slice包括一个元素指针、一个长度、一个容量。对应的bitmap位图中第一字节的第0位到第2位标记指针/标量,第一字节的第4位到第6位标记是否需要继续扫描

  • pageInUse一个uint8类型的数组,长度为1024,所以总共有8192位,它的作用就是标记哪些页被使用了。但实际上,pageInUse只标记处于使用状态的span的第一个page。

    image-20230211204542195

    例如在这张图中,第一个使用状态的span包括两个page,对应的pageInUse第0位标记为1

    第二个使用状态的span包括三个page,对应的pageInUse的第2位标记为1

  • pageMarks这个位图跟GC标记有点关系,它的用法跟pageInUse一样,只标记span的第一个page

    在GC标记阶段会修改这个位图,会标记哪些span中存在被标记的对象(被GC标记说明是存活对象,没有被标记说明要被回收)

    在标记清扫阶段会根据这个位图,来释放不含标记对象的span

    image-20230211204859830

  • spans是一个*mspan类型的数组,长度为8192,正好对应arena当中8192个page,所以用来定位一个page的mspan在哪里。

    image-20230211210423061

mspan

image-20230211210105919

mspan管理着span中一组连续的page。同mcentral一样,将划分的内存规格记录到spanclass当中,nelem记录着当前span一共被划分成多少内存块,freeIndex记录着下一块空闲的内存块索引

与heapArena不同,mspan的位图标记面向的是划分好的内存块单元

allowBits位图:用来标记哪些内存块被标记了(被GC标记说明是存活对象)

gcmarkBits位图:是当前span的标记位图,在GC标记阶段会对这个位图进行标记,一个二进制位对应span中的一个内存块

在GC清扫阶段会释放掉旧的allowBits,指向gcmarkBits

这样未被GC标记的就能够被回收利用了

当然会重新分配一个清零的内存给gcmarkBits位图。

总结

总结了堆堆基本机构与管理堆的主要数据结构

关键是理解mcentral和mcache的工作模式,以及heapArea和mspan中重要的位图标记

go内存管理

学习地址:www.bilibili.com/video/BV1av…

参考博客地址:draveness.me/golang/docs…