超全代码!详解Go中内存分配源码实现( 二 )


在runtime.mcentral中 , 有spanclass标识 , spanclass表示这个mcentral的类型 , 下面我们会看到 , 在分配[16B,32KB]大小对象的时候 , 会将对象的大小分成67组:
所以runtime.mcentral只负责一种spanclass规格类型 。
runtime.mcentral的数据会由两个spanSet托管 , partial负责空闲的列表 , full负责已被使用的列表 。
spanSet这个数据结构里面有一个由index组成的头尾指针 , pop数据的时候会从头获取 , push数据的时候从tail放入 , spine相当于数据块的指针 , 通过head和tail的位置可以算出每个数据块的具体位置 , 数据块由spanSetBlock表示:
spanSetBlock是一个存放mspan的数据块 , 里面会包含一个存放512个mspan的数据指针 。 所以mcentral的总体数据结构如下:
超全代码!详解Go中内存分配源码实现
文章图片
runtime.mheap
对于runtime.mheap需要关注central和arenas 。 central是各个规格的mcentral集合 , 在初始化的时候会通过遍历class_to_size来进行创建;arenas是一个二维数组 , 用来管理内存空间 。 arenas由多个runtime.heapArena组成 , 每个单元都会管理64MB的内存空间:
需要注意的是 , 上面的heapArenaBytes代表的64M只是在除windows以外的64位机器才会显示 , 在windows机器上显示的是4MB 。 具体可以看下面的官方注释:
L1entries、L2entries分别代表的是runtime.mheap中arenas一维、二维的值 。
超全代码!详解Go中内存分配源码实现
文章图片
2.给对象分配内存
我们通过对源码的反编译可以知道 , 堆上所有的对象都会通过调用runtime.newobject函数分配内存 , 该函数会调用runtime.mallocgc:
通过mallocgc的代码可以知道 , mallocgc在分配内存的时候 , 会按照对象的大小分为3档来进行分配:
1)小于16bytes的小对象;
2)在16bytes与32K之间的微对象;
3)大于32Kb的大对象;
3.大对象分配
从上面我们可以看到分配大于32KB的空间时 , 直接使用largeAlloc来分配一个mspan 。
在分配内存的时候是按页来进行分配的 , 每个页的大小是_PageSize(8K) , 然后需要根据传入的size来判断需要分多少页 , 最后调用alloc从堆上分配 。
继续看allocSpan的实现:
这里会根据需要分配的内存大小再判断一次:
如果要分配的页数小于pageCachePages/4=64/4=16页 , 那么就尝试从pcache申请内存;
如果申请的内存比较大或者线程的页缓存中内存不足 , 会通过runtime.pageAlloc.alloc从页堆分配内存;
如果页堆上内存不足 , 那么就mheap的grow方法从系统上申请内存 , 然后再调用pageAlloc的alloc分配内存;
下面来看看grow的向操作系统申请内存:
sysAlloc方法会调用runtime.linearAlloc.alloc预先保留的内存中申请一块可以使用的空间;如果没有会调用sysReserve方法会从操作系统中申请内存;最后初始化一个heapArena来管理刚刚申请的内存 , 然后将创建heapArena放入到arenas列表中 。
至此 , 大对象的分配流程至此结束 。
4.小对象分配
对于介于16bytes~32K的对象分配如下:
首先会先计算sizeclass大小 , 计算sizeclass是通过预先定义两个数组:size_to_class8和size_to_class128 。 小于1024-8=1016(smallSizeMax=1024) , 使用size_to_class8 , 否则使用数组size_to_class128 。
举个例子 , 比如要分配20byte的内存 , 那么sizeclass=size_to_calss8[(20+7)/8]=size_to_class8[3]=3 。 然后通过class_to_size[3]获取到对应的值32 , 表示应该要分配32bytes的内存值 。
接着会从alloc数组中获取一个span的指针 , 通过调用nextFreeFast尝试从mcache中获取内存 , 如果mcache不够用了 , 则尝试调用nextFree从mcentral申请内存到mcache 。
下面看看nextFreeFast:
allocCache在初始化的时候会初始化成^uint64(0) , 换算成二进制 , 如果为0则表示被占用 , 通过allocCache可以快速的定位待分配的空间:
nextFree中会判断当前span是不是已经满了 , 如果满了就调用refill方法从mcentral中获取可用的span , 并替换掉当前mcache里面的span 。
Refill根据指定的sizeclass获取对应的span , 并作为mcache的新的sizeclass对应的span 。
cacheSpan主要是从mcentral的spanset中去寻找可用的span , 如果没找到那么调用grow方法从堆中申请新的内存管理单元 。
获取到后更新nmalloc、allocCache等字段 。
到这里小对象的分配就讲解完毕了 。
5.微对象分配
在分配对象内存的时候做了一个判断 , 如果该对象的大小小于16bytes , 并且是不包含指针的 , 那么就可以看作是微对象 。
在分配微对象的时候 , 会先判断一下tiny指向的内存块够不够用 , 如果tiny剩余的空间超过了size大小 , 那么就直接在tiny上分配内存返回;