Go 内存管理

发布于 2022-01-15 12:48:01 字数 7030 浏览 1208 评论 0

这里的内存管理一般指的是堆内存管理,因为栈上的内存分配和回收非常简单,不需要程序操心,而堆内存需要程序自己组织、分配和回收,用于动态分配内存。Golang内存管理的主要思想源自Google 的 TCMalloc 算法,全称 Thread-Caching Malloc,核心思想就是把内存分为多级管理,从而降低锁的粒度。即为每个线程预分配一块缓存(Thread-cache),线程申请小内存时,可以从缓存分配内存,这样做有两个好处:

  1. 不必每次申请内存时都向操作系统申请,避免了系统调用,提升速度
  2. 由于这块缓存(Thread-cache)是每个线程独有的,因此不存在多个线程竞争的问题,多个线程同时申请小内存时,从各自的缓存分配,无需加锁,进一步提升速度

TCMalloc算法这里就跳过了,直接介绍 Go 的内存管理,两者比较相似。

首先介绍一下 Go 内存管理的基本概念和数据结构:

Page

操作系统对内存管理以页为单位,不过这里的页不是操作系统中的页,它一般是操作系统页大小的几倍,x64 下 Page 大小是 8KB

mspan

Go中内存管理的基本单元,一组连续的Page组成1个Span。mspan这个数据结构主要包含以下信息:

  • 链表中上一个和下一个mspan的地址(简单说,mspan是一个双向链表
  • 起始地址(这组page的起始地址)
  • spanClass,一个0~numSpanClasses(常量,134)之间的值,可以理解为对这个 mspan 的分类,或者叫它的规格,之后详细介绍,简单说就是不同spanClass的mspan可以存储的对象大小是不一样的

mcache

与 Thread-Cache 类似,每个线程绑定一个 mcache(具体来说是每个P绑定一个 mcache)。这样小对象直接从 mcache 分配内存,不用加锁

mcache 这个数据结构中保存了各种 spanClass 的 mspan

type mcache struct {
    alloc [numSpanClasses]*mspan // numSpanClasses = = _NumSizeClasses << 1,即2*67 = 134
}

mcache中的alloc是一个大小为134的数组,其中的每个元素都是一个mspan双向链表,并且同一个链表上的内存块大小是相同的,相当于按照spanClass给不同规格的mspan分类存储在数组中进行管理(可以参考上面的图),这样可以根据申请的内存大小,快速从合适的mspan链表选择空闲内存块。

mcentral

为所有mcache提供按照spanClass分好类的mspan资源(实际代码中每1个spanClass对应1个mcentral),当某个mcache的某个spanClass的mspan中的内存被分配光时,它会向mcentral申请一个对应spanClass的mspan。当mcache内存块多时,可以放回mcentralmcentral被所有工作线程共享,因此需要加锁访问(获取和归还)

// 保留重要成员变量
type mcentral struct {
    // 互斥锁
    lock mutex 

    // 规格
    sizeclass int32 

    // 尚有空闲object的mspan链表
    nonempty mSpanList 

    // 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
    empty mSpanList 

    // 已累计分配的对象个数
    nmalloc uint64 
}

mcachemcentral获取和归还mspan的流程:

  • 获取 加锁;从nonempty链表找到一个可用的mspan;并将其从nonempty链表删除;将取出的mspan加入到empty链表;将mspan返回给工作线程;解锁。
  • 归还 加锁;将mspanempty链表删除;将mspan加入到`nonempty链表;解锁。

mheap

是堆内存的抽象,把从OS申请出的内存页组织成mspan,并保存起来。当mcentral没有空闲的mspan时,会向mheap申请。而mheap没有资源时,会向操作系统申请新内存。同样需要加锁访问

mheap里保存了2棵二叉排序树(见第一张大图),按mspan的page数量进行排序:

  1. freefree 中保存的 mspan 是空闲并且非垃圾回收的 mspan
  2. scavscav 中保存的是空闲并且已经垃圾回收的 mspan

如果是垃圾回收导致的 mspan 释放,mspan 会被加入到 scav,否则加入到 free,比如刚从OS申请的的内存也组成的 mspan

堆区总览:

主要关注图里的spansarena区域,spans区域存放mspan的指针,而arena区域就是实际分配内存的地方,被分割成以页为单位,再把页组合起来成为Go的内存管理的基本单元mspanmspan数据结构里面存放的起始地址信息就是指向的arena区域。bitmap区域标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。

内存分配

当为一个对象分配内存时,Golang首先根据申请的内存大小将对象进行分类:

Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象,使用mcache的tiny分配器直接分配;

而超过32KB的大对象直接从mheap上分配,与mcentralmheap申请内存的流程大致相同;

下面主要介绍小对象的内存分配流程。

之前说过,mspan是Golang内存管理的基本单元,所以当小对象申请内存时,Golang需要做的就是:从mcache中寻找合适的mspan进行分配;而mcache中保存的mspan双向链表又是以spanClass进行分类的(mcache中的alloc数组),所以第一步就是计算出对象申请的内存大小对应的spanClass:

  1. 计算sizeClass,因为得到了sizeClass我们才能计算出spanClass。在Golang里sizeClass一共有67种,可以理解为对内存大小的一个分类,不同sizeClass可以保存的大小是不一样的。每个sizeClass可以保存的大小是用一个数组写死在了源码里(空间换时间):
const _NumSizeClasses = 67

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

举个例子,如果一个对象大小在(0, 8]byte之间,对应的sizeClass就是1(往右取数组下标),对象大小在(8, 16]byte之间,对应的sizeClass就是2

  1. 根据sizeClass计算spanClass
numSpanClasses = _NumSizeClasses << 1 // 2 * 67 = 134

可以发现sizeClass一共是67,而这里spanClass是_NumSizeClasses的两倍,原因在于为了加速之后内存回收的速度,mspan也是做了区分的,在mcache中的alloc数组里保存的mspan,有一半分配的对象不包含指针,另一半则包含指针,对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。

sizeClass到spanClass的计算如下:

// noscan为true代表对象不包含指针
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
    return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}

得到spanClass之后,就可以从mcache中选择相应的mspan进行分配;如果mcache中没有相应规格的mspan,则会向mcentral申请;如果mcentral没有合适的mspannonemptyempty链表里都没有合适的mspan),则会向mheap申请;如果mheap没有,则会向操作系统申请。

mcentralmheap申请时,mheap优先从free中搜索可用的mspan,如果没有找到,会从scav中搜索可用的mspan,如果还没有找到,它会向OS申请内存,再重新搜索2棵树,必然能找到mspan。如果找到的mspan比需求的大,则将其分割成2个mspan,其中1个刚好是需求大小,把剩下的再加入到free中去,然后设置需求mspan的基本信息,然后交给mcentral

推荐阅读

推荐两篇不错的文章,结合着看,更加清晰:

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84961 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文