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

导语|本文会从调试汇编入手讲解Go的内存分配实现的源码 , 所以在看的时候不妨自己动手调试 , 并且文中含有大量高质量的图片帮助理解 , 耐心看完一定能有所收获 。 注:本文使用的go源码为15.7 。
一、介绍
Go语言的内存分配器借鉴了TCMalloc的设计实现高速的内存分配 , 它的核心理念是使用多级缓存将对象根据大小分类 , 并按照类别实施不同的分配策略 。 TCMalloc相关的信息可以看这里:
即如果要分配的对象是个小对象(
如下:对象被分到不同的内存大小组中的链表中 。
超全代码!详解Go中内存分配源码实现
文章图片
如果是个大对象(>32k) , 那么页堆进行分配 。 如下:
超全代码!详解Go中内存分配源码实现
文章图片
虽然go内存分配器最初是基于tcmalloc的 , 但是现在已经有了很大的不同 。 所以上面的一些结构会有些许变化 , 下面会提及 。
因为内存分配的源码比较复杂 , 为了方便大家调试 , 所以在进行源码分析之前 , 先看看如何断点汇编进行调试 。
1.断点调试汇编
目前Go语言支持GDB、LLDB和Delve几种调试器 。 只有Delve是专门为Go语言设计开发的调试工具 。 而且Delve本身也是采用Go语言开发 , 对Windows平台也提供了一样的支持 。 本节我们基于Delve简单解释如何调试Go汇编程序 。 项目地址:https://github.com/go-delve/delve
安装:
首先编写一个test.go的例子:
然后命令行进入包所在目录 , 然后输入dlvdebug命令进入调试:
然后可以使用break命令在main包的main方法上设置一个断点:
通过breakpoints查看已经设置的所有断点:
通过continue命令让程序运行到下一个断点处:
通过disassemble反汇编命令查看main函数对应的汇编代码:
现在我们可以使用break断点到runtime.newobject函数的调用上:
输入continue跳到断点的位置:
print命令来查看typ的数据:
可以看到这里打印的size是16bytes , 因为我们A结构体里面就一个string类型的field 。
进入到mallocgc方法后 , 通过args和locals命令查看函数的参数和局部变量:
2.各个对象入口
我们根据汇编可以判断 , 所有的函数入口都是runtime.mallocgc , 但是下面两个对象需要注意一下:
int64对象
runtime.convT64
这段代码表示如果一个int64类型的值小于256 , 直接使用的是缓存值 , 那么这个值不会进行内存分配 。
string对象
runtime.convTstring
由这段代码显示 , 如果是创建一个为”“的string对象 , 那么会直接返回一个固定的地址值 , 不会进行内存分配 。
3.调试用例
大家在调试的时候也可以使用下面的例子来进行调试 , 因为go里面的对象分配是分为大对象、小对象、微对象的 , 所以下面准备了三个方法分别对应三种对象的创建时的调试 。
二、分析
1.分配器的组件
内存分配是由内存分配器完成 , 分配器由3种组件构成:runtime.mspan、runtime.mcache、runtime.mcentral、runtime.mheap 。
runtime.mspan
runtime.mspan是内存管理器里面的最小粒度单元 , 所有的对象都被管理在mspan下 。
mspan是一个链表 , 有上下指针;
npages代表mspan管理的堆页的数量;
freeindex是空闲对象的索引;
nelems代表这个mspan中可以存放多少对象 , 等于(npages*pageSize)/elemsize;
allocCache用于快速的查找未被使用的内存地址;
elemsize表示一个对象会占用多个bytes , 等于class_to_size[sizeclass] , 需要注意的是sizeclass每次获取的时候会sizeclass方法 , 将sizeclass>>1;
limit表示span结束的地址值 , 等于startAddr+npages*pageSize;
实例图如下:
超全代码!详解Go中内存分配源码实现
文章图片
图中alloc是一个拥有137个元素的mspan数组 , mspan数组管理数个page大小的内存 , 每个page是8k , page的数量由spanclass规格决定 。
runtime.mcache
runtime.mcache是绑在并发模型GPM的P上 , 在分配微对象和小对象的时候会先去runtime.mcache中获取 , 每一个处理器都会被分配一个线程缓存runtime.mcache , 因此从runtime.mcache进行分配时无需加锁 。
在runtime.mcache中有一个alloc数组 , 是runtime.mspan的集合 , runtime.mspan是Go语言内存管理的基本单元 。 对于[16B,32KB]的对象会使用这部分span进行内存分配 , 所以所有在这区间大小的对象都会从alloc这个数组里寻找 , 下面会分析到 。
runtime.mcentral
当runtime.mcache中空间不足的时候 , 会去runtime.mcentral中申请对应规格的mspan 。 获取mspan的时候会从partial列表和full列表中获取 , 获取的时候会使用无锁的方式获取 。