Go内存分配器的设计与实现
- 函数调用的参数,返回值,局部变量基本都分配在栈上。
- 内存管理一般包含三个不同的组件,分别是用户程序,分配器和收集器。
- 一般有两种内存分配方法,一种是线性分配器,另一种是空闲链表分配器。 线性分配器 线性分配(Bump Allocator)是一种高效的内存分配方法。当我们在编程语言中使用线性分配器,我们只需要在内存中维护一个指向内存特定位置的指针,当用户程序申请内存时,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即标记已经被使用了的内存。 因为线性分配器足够简单,可以有较快的执行速度和实现复杂度,但是垃圾回收存在局限性,需要配合压缩标记,复制回收和分代回收这种通过拷贝的方式整理存活对象的碎片,将空闲内存定时合并。 空闲链表回收器 不同的内存块以链接的方式链接,这种方式可以方便的回收内存资源,但是在分配内存的时候需要遍历链表,所以分配内容的时候时间复杂度是O(n)。 内存分配策略有四种方式:首次适应,循环首次适应,最优适应,隔离适应
go语言的内容分配
- go语言使用的内存策略类似隔离适应。隔离适应是把内存隔离成4,8,16,32的内存块组成的链表,然后当我们向内存分配器申请8字节的内容时,遍历8字节的链表,直到找到空的内存块并返回。减少了遍历内存的长度,提高了效率。
- go利用多级缓存,将对象根据大小分类,并按照类型实施不同的分配策略。分成线程缓存,中心缓存,页堆三个组件。
image-20200525104154084
- go利用分别对待处理对象大小来处理内存分配,从而提高分配效率。 类别 大小 微对象 (0, 16B) 小对象 [16B, 32KB] 大对象 (32KB, +∞)
- 通过多级缓存可以减少锁次数,当线程缓存不能满足需求时,就会使用中心内存补充解决小对象的内容分配问题,在遇到32kb以上的对象时,内存分配器会直接使用页堆直接分配大量内存。
- go语言在1.10版本的时候还是连续内容。Go 语言在垃圾回收时会根据指针的地址判断对象是否在堆中,建立在堆区的内存是连续的这一假设上,才能找到内存管理单元,这种设计虽然简单并且方便,但是在 C 和 Go 混合使用时会导致程序崩溃:分配的内存地址会发生冲突,导致堆的初始化和扩容失败;没有被预留的大块内存可能会被分配给 C 语言的二进制,导致扩容后的堆不连续。
image 程序内存根据自己所在位置的基地址算到spans所在的数组位置,从而找到属于它的内容管理单元。
- 在1.11使用了稀疏的内存布局。不过因为基于稀疏内存的内存管理失去了内存的连续性这一假设,这也使内存管理变得更加复杂,但是解决了上述相关的问题。
image-20200525105921837
runtime.heapArena稀疏的内容布局不再是直接算的管理单元,而是直接指向。
由于内存的管理变得更加复杂,上述改动对垃圾回收稍有影响,大约会增加 1% 的垃圾回收开销。
内存状态变化
四种状态
go内存管理对象结构
image-20200525114545568
runtime.mspanruntime.mspanruntime.mspanallocCacheruntime.mcacheruntime.class_to_sizeruntime.class_to_allocnpagesnoscanruntime.mspanruntime.mcache
runtime.mcacheruntime.mspanalloc这三个字段组成了微对象分配器,专门为 16 字节以下的对象申请和管理内存
runtime.mcentral
runtime.mcentralnmallocruntime.mcentral.cacheSpan- 从empty查找。
- 从nonempty查找,看看有没有被标记回收或者已经回收的,插回到empty中。
- 遍历nonempty看看有没有内存可以回收的。
- 调用runtime.mcentral.grow从堆中申请新的内容管理单元。
- 更新allocCache等字段。
runtime.mheap
runtime.mheapcentralarenasruntime.mcentralscannoscan初始化:
spanalloccacheallocarenaHintAllocruntime.fixalloccentralruntime.mcentral这会帮助分配器分割待分配的内存,该分配器提供了以下两个用于分配和释放内存的方法:
runtime.fixalloc.allocruntime.fixalloc.free除了这些空闲链表分配器之外,我们还会在该方法中初始化所有的中心缓存,这些中心缓存会维护全局的内存管理单元,各个线程会通过中心缓存获取新的内存单元。
用户程序申请内容流程
runtime.newobjectruntime.mallocgcruntime.gomcacheruntime.mallocgc(0, 16B)[16B, 32KB](32KB, +∞)微对象
Go 语言运行时将小于 16 字节的对象划分为微对象,它会使用线程缓存上的微分配器提高微对象分配的性能,我们主要使用它来分配较小的字符串以及逃逸的临时变量。微分配器可以将多个较小的内存分配请求合入同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。
maxTinySizemaxTinySizemaxTinySizeruntime.mcachetinymaxTinySizeruntime.mspanruntime.nextFreeFastruntime.mcache.nextFreetinytinyoffset小对象
小对象是指大小为 16 字节到 32,768 字节的对象以及所有小于 16 字节的指针类型的对象,小对象的分配可以被分成以下的三个步骤:
runtime.spanClassruntime.memclrNoHeapPointers大对象
runtime.largeAlloc