为什么需要内存分配器? 总说周知,内存作为一种相对稀缺的资源,在操作系统中以虚拟内存的形式来作为一种内存抽象提供给进程,这里可以简单地把它看做一个连续的地址集合{0, 1, 2, ..., M},由栈空间、堆空间、代码片、数据片等地址空间段组合而成,如下图所示(出自CS:APP3e, Bryant and O’Hallaron的第9章第9节)
这里我们重点关注Heap(堆),堆是一块动态的虚拟内存地址空间。在C语言中,我们通常使用malloc来申请内存以及使用free来释放内存,也许你想问,这样不就足够了吗?但是,这种手动的内存管理会带来很多问题,比如:
给程序员带来额外的心智负担,必须得及时释放掉不再使用的内存空间,否则就很容易出现内存泄露 随着内存的不断申请与释放,会产生大量的内存碎片,这将大大降低内存的利用率 因此,正确高效地管理内存空间是非常有必要的,常见的技术实现有Sequential allocation, Free-List allocation等。那么,在Go中,内存是如何被管理的呢?
注:此为Go1.13.6的实现逻辑,随版本更替某些细节会有些许不同
实现原理 Go的内存分配器是基于TCMalloc设计的,因此我建议你先行查阅,这将有利于理解接下来的内容。
大量工程经验证明,程序中的小对象占了绝大部分,且生命周期都较为短暂。因此,Go将内存划分为各种类别(Class),并各自形成Free-List。相较于单一的Free-List分配器,分类后主要有以下优点:
其一方面减少不必要的搜索时间,因为对象只需要在其所属类别的空闲链表中搜索即可
另一方面减少了内存碎片化,同一类别的空闲链表,每个对象分配的空间都是一样大小(不足则补齐),因此该链表除非无空闲空间,否则总能分配空间,避免了内存碎片
那么,Go内存分配器具体是如何实现的呢?接下来,我将以自顶向下的方式,从宏观到微观,层层拨开她的神秘面纱。
数据结构 首先,介绍Go内存分配中相关的数据结构。其总体概览图如下所示:
heapArena 在操作系统中,我们一般把堆看做是一块连续的虚拟内存空间。
Go将其划分为数个相同大小的连续空间块,称之arena,其中,heapArena则作为arena空间的管理单元,其结构如下所示:
type heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan ... } bitmap: 表示arena区域中的哪些地址保存了对象,哪些地址保存了指针 spans: 表示arena区域中的哪些操作系统页(8K)属于哪些mspan mheap 然后,则是核心角色mheap了,它是Go内存管理中的核心数据结构,作为全局唯一变量,其结构如下所示:
type mheap struct { free mTreap ... allspans []*mspan ... arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena ... central [numSpanClasses]struct { mcentral mcentral pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } } free: 使用树堆的结构来保存各种类别的空闲mspan allspans: 用以记录了分配过了的mspan arenas: 表示其覆盖的所有arena区域,通过虚拟内存地址计算得到下标索引 central: 表示其覆盖的所有mcentral,一共134个,对应67个类别 mcentral 而mcentral充当mspan的中心管理员,负责管理某一类别的mspan,其结构如下:...