TCMalloc
go内存管理比较复杂,它借鉴了谷歌的TCMalloc内存分配算法,全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free,new,new等)。 它的好处就是程序启动的时候先向操作系统申请一大块内存,然后根据规则切割成小内存块,根据对象的大小分配对应规格的内存块,并且在回收的时候也并不是直接还给操作系统,而是自己管理空闲的内存,这样下一次有新对象的时候,直接分配,不需要找操作系统了,减少和操作系统这种往来的开销。
go内存结构
几个概念 :
- 虚拟内存:假设现在有个Go代码A, A逻辑都非常复杂,代码都1亿行,占用10G,而此时我的电脑内存一共才8G,还运行一大堆软件,所剩内存无几,那么代码A是不是就不能运行了?答案肯定不是,我们平时代码中看到的地址都是虚拟地址,通过MMU进行虚拟地址转换成为物理地址,加载代码,一条一条运行指令,然后释放虚拟内存。
- 栈区:从高地址开始,向低地址扩展,内存管理简单,不需要自己free,系统自动回收,GC友好
- 堆区:从低地址开始,向高地址扩展,申请和释放需要自己动手,操作麻烦。
我们说的go的内存管理,其实就是对 堆内存 的管理,主要通过runtime来分配和管理,不需要程序员自己去new和free。
mspan
go程序在初始化的时候,会去申请一大块的虚拟内存(并不是真正的内存空间) arena ,这块内存的大小是512G,这就是堆内存。因为操作系统是页式存储,所以arena本质也是由一页一页组成的。
这里的一个page=8k,在现实情况中,有时候申请的对象可能很小,也有可能很大,于是不同的page数量组成了不同的mspan。
go根据class的size,又分为67种mspan。
这里列出了66种,还有一种大于32k的大对象(class=0)从mheap直接分配,下面会讲。 解释下sizeclass:
- 第一列为class的索引,递增
- 第二列为对象的大小
- 第三列为span的大小
- 第四列为可以存储的对象数
- 第五列tail waste是span%obj计算的结果,因为span的大小并不一定是对象大小的整数倍。
- 最后一列max waste代表最大浪费的内存百分比
假设现在要申请一个对象的大小为16B,对应的class是2,关联的span是8192,也就是一个page,那么对于这个对象它就能申请到一个页组成的mspan。mspan里面又按照8*2n大小(8b,16b,32b …. )分割,每一个mspan又分为多个object,go将相同class的mspan以链表的方式连在一起。
heap bitmap
堆内存已经申请到,并且根据object大小,分成了不同类型的mspan。但是当我们申请内存时,这些一块一块的object,我们怎么知道是不是被用了?于是bitmap出现了,bitmap主要用于标识每个object块使用状态,是否被GC标记过。bitmap中一个byte大小的内存对应arena区域中4个指针大小(指针大小为 8B )的内存,4位标记object是否被使用,4位标记object是否被GC标记,所以bitmap区域的大小是512GB/(4*8B)=16GB。
mcache
每个m会绑定一个p,每个p在某个时间点只能运行一个goroutine,每个p都有一个mcache(本地分配器),当申请一个小于32k的对象时,会优先从本地的mcache中查找可用的mspan。这样的好处就是基于本地p,寻找mspan的时候不需要加锁,效率高。如果没有mcache,类似只有一块全局的内存,那么每个goroutine去申请的时候,存在竞争,必然要加锁。
每个mcache持有一批mspan,不同的sizeclass系列,上面说到有67种mspan,但实际上有2*67种,每个sizeclass对应指针类型和非指针类型的,对于非指针类型的object,在GC的时候可以快速回收,不需要顺着指针一层一层找。
mcentral
当mcache中没有找到可用的mspan的时候,会去向mcentral申请。mcentral会为所有的mcache提供切割好的mspan,mcentral根据上面的sizeclass分为67*2种(指针和非指针类型),每种mcentral管理一种mspan。
- lock:因为mcentral是全局的,被所有线程M共享,所以从mcentral获取mspan时要加锁
- spanclass:每种mcentral负责的mspan类型
- noempty:双端链表,表示还有空闲的mspan可以使用
- empty:双端链表,表示没有空闲的mspan可以使用,已经被mcache取走还没有归还的mspan
mcache向mcentral申请的流程:
- 根据对象的大小,找到对应的sizeclass的mcentral
- 获取加锁,从noempty的链表中找到一个可用的mspan,并将其从noempty中删除,将取出的mspan加入到empty链表中,mspan返回给工作线程,解锁。
- 归还加锁,将mspan从empty链表中删除,将mspan加入noempty链表中,解锁。
因为根据spanclass可以分为不同的mcentral,所以不同的工作线程在获取不同类型的mcentral时,互不干扰。
mheap
当mcentral也没有可用的mspan,会向真正全局的mheap申请,mheap管理的就是一开始申请的堆内存,mheap不负责切割内存。如果mheap没有足够的内存,那么会向操作系统申请一大块内存,对于大于32k的对象不会经过mcache、mcentral分配器,而会直接从mheap上申请对应数量的page给应用程序。
tiny分配器
按照上面的规则,假设现在有个int32变量占4b,那么应该获取sizeclass=1的对应的mspan,每个object是8b,那么在int32用掉4b后,还剩4b。对于bool占用一个字节,还是7b。可以发现对于这些小对象,可能会造成很多的内碎片。
针对这些小对象,tiny分配器决定对于<=16b的内存申请时,都会从spanclass=2的对应的规格中获取一个16b的对象以供使用,并且多个对象可以复用这个16b的空间,争取把内碎片降到最低。
- 申请int16空间,占用2b
- 获取一个16b的object
- 申请int32空间,复用之前的object,占用4b
- 申请int64空间,复用之前的object,占用8b
- object剩余2b
真实的情况还要考虑内存对齐的问题,这里只是简单说明。
总结:obj<=16b的走tiny分配器, 16b<obj<=32k走mcache->mcentral->mheap这套流程,obj>32k直接走mheap分配。