内存分配器
程序中的数据和变量都会被分配到程序所在的虚拟内存中,内存空间包含两个重要的区域:栈区(Stack)和堆区(Heap)
- 函数调的参数,返回值,局部变量大部分会分配到栈上,这部分内存会用编译器进行管理。调用顺序是先进后出
不同编程语言使用不同的方法管理堆内存, C++ 等编出语言由工程师和申请和释放,Go 与 Java 由工程师和编译器共同管理。堆中的对象由内存分配期分配并由垃圾收集器回收。
内存管理基础概念
内存管理一般包含三个不同的组件,分别是用户程序(Mutator), 分配器(Allocator) 和收集器(Collector) 当用户程序申请内存时,会通过内存分配器申请新内存,内存分配器会从堆中除时候相应的内存区域。
内存管理有两个重要部分: 栈区 Stack 和堆区 Heap。函数调用的参数,返回值,局部变量大部分都分配到栈上,编译器负责管理,
如何分配内存
编程语言的内存分配方法:一种是线性分配器(Sequential Allocator) ,一种是空闲链表分配器(Free-List Allocator)
线性分配器
线性分配器是一种高效的内存分配方法。
使用线性分配器分配内存时, 只需要在内存中维护一个指向内存特定位置都指针。 当程序申请内存时, 分配器只需要检查剩余的空闲内存,返回分配的内存区域并修改指针在内存中的位置。优点: 执行快,容易实现
-
空闲链表分配器
空闲链表分配器可以重用已经被释放的内存,在内部会维护一个类似链表的数据结构。当用户申请内存时, 空闲链表分配器会一次便利空闲的内存块,找到足够大的内存,然后分配资源,并修改链表。
优点: 方便内存复用
-
空闲看表分配策略
首次适应
循环首次适应
从上次遍历的技术位置开始遍历, 选择第一个内存大小大于申请内存的内存块
最优适应
隔离适应
将内存分割成多个链表,该策略下会将内存分割成4,8,16,32 字节的内存,申请内存时先找到满足内存大小的链表,再从链表中选择合适的内存块。
分级分配
线程缓存分配器(TCMollc) 是用于分配内存的机制。 Go 语言的内存分配器借鉴了多级分配的思想,实现了高速的内存分配。
Go 语言中时根据 对象的大小,进行不同的处理逻辑, 运行时根据对象的大小将对象分配成微对象, 小对象,大对象。
总的来说, 分配成3个级别 线程缓存(Thread Cache)
- 中心缓存(Central Cache)
- 页堆(Page Heap)
线程缓存和线程时一对一的关系, 一般来说,线程缓存能满足大部分的内存分配需求, 因为不涉及多内存之间的共享设计,这样也不需互斥锁实现线程安全。
当线程缓存不满足需求时, 运行时就会使用中心缓存(Central Cache) 来保护内存,可以通过线程缓存和中心缓存提供足够的内存空间。
如果遇到 32K 以上的对象,内存分配器会选择堆直接分配大内存。
这个多层级的内存设计与计算机操作系统中的多级缓存有点类似。
Go 上如何管理和分配内存
Go 使用三个组件分级管理内存, 包括内存管理单元( runtime.mspan) 线程缓存(runtime.mcache) 中心缓存(runtime.mcentral) 和页堆(runtime.mheap) 组件。
线性内存
Go 语言程序的 1.10 版本在启动时会初始化整片虚拟内存区域,如下所示的三个区域 spans、bitmap 和 arena 分别预留了 512MB、16GB 以及 512GB 的内存空间, 这些不是物理内存,而是虚拟内存。
- spans 区域存储了指向内存管理单元 runtime.mspan 的指针,每个内存单元会管理几页的内存空间,每页大小为 8KB;
- bitmap 用于标识 arena 区域中的那些地址保存了对象,位图中的每个字节都会表示堆区中的 32 字节是否空闲;
arena 区域是真正的堆区,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象;
内存管理单元
runtime.mspan 时 Go 内存管理的基本单元, 结构体中包含了next 和 prev 两个字段。结构如下: ```go type mspan struct { next mspan // next span in list, or nil if none prev mspan // previous span in list, or nil if none list *mSpanList // For debugging. TODO: Remove.
startAddr uintptr // address of first byte of span aka s.base() 起始地址 npages uintptr // number of pages in span // 页数
manualFreeList gclinkptr // list of free objects in mSpanManual spans freeindex uintptr // TODO: Look up nelems from sizeclass and remove this field if it // helps performance. nelems uintptr // number of object in the span. allocCache uint64 // allocBits 补码,可以用于快速查找内存中未被使用的内存 allocBits gcBits. // 标记内存占用 gcmarkBits gcBits // 标记内存回收 …
}
类似于数据结构中双链表的结构, 每个 runtime.mspan 都管理 npages 个大小的 8KB 的页 ,这里的页不是操作系统的内存页, 是操作系统页的整数倍。<a name="aupQk"></a>##### 微分配器线程缓存中包含微分配器对象的字段<a name="cD7fu"></a>#### 中心缓存runtime.mcentral 是内存分配器的中新缓存,与线程缓存不同,访问中心缓存需要使用互斥锁。```go////go:notinheaptype mcentral struct {spanclass spanClass// partial and full contain two mspan sets: one of swept in-use// spans, and one of unswept in-use spans. These two trade// roles on each GC cycle. The unswept set is drained either by// allocation or by the background sweeper in every GC cycle,// so only two roles are necessary.//// sweepgen is increased by 2 on each GC cycle, so the swept// spans are in partial[sweepgen/2%2] and the unswept spans are in// partial[1-sweepgen/2%2]. Sweeping pops spans from the// unswept set and pushes spans that are still in-use on the// swept set. Likewise, allocating an in-use span pushes it// on the swept set.//// Some parts of the sweeper can sweep arbitrary spans, and hence// can't remove them from the unswept set, but will add the span// to the appropriate swept list. As a result, the parts of the// sweeper and mcentral that do consume from the unswept list may// encounter swept spans, and these should be ignored.partial [2]spanSet // list of spans with a free objectfull [2]spanSet // list of spans with no free objects}
mcentral 管理连哥哥 span 一个是 noempty (partial)里面有空闲的soan, 另外是一个无空闲的 span (full)。
页堆
runtime.mheap 是内存分配的核心结构,这个存储全局变量,堆上初始化的所有对象由该结构统一管理。该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。
type mheap struct {// lock must only be acquired on the system stack, otherwise a g// could self-deadlock if its stack grows with the lock held.lock mutexpages pageAlloc // page allocation data structuresweepgen uint32 // sweep generation, see comment in mspan; written during STWsweepdone uint32 // all spans are sweptsweepers uint32 // number of active sweepone calls// allspans is a slice of all mspans ever created. Each mspan// appears exactly once.//// The memory for allspans is manually managed and can be// reallocated and move as the heap grows.//// In general, allspans is protected by mheap_.lock, which// prevents concurrent access as well as freeing the backing// store. Accesses during STW might not hold the lock, but// must ensure that allocation cannot happen around the// access (since that may free the backing store).allspans []*mspan // all spans out there_ uint32 // align uint64 fields on 32-bit for atomicscentral [numSpanClasses]struct {mcentral mcentralpad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte}arena linearAlloc...}
微/小对象申请过程
如果是创建一个微或者小对象,就会向 mcache 进行申请。mcache 如果没有空闲的 span 的话,会向 mcentral 申请。mcentral 管理着两个 spanlist,一个是 partial(里面有空闲的 span),一个是 full(无空闲的链表),mcentral 会从 partial 找到可用的 span,将其移至 empty list,并将其返回给线程。当 mcentral 也没有空闲时 span,会向 mheap 申请。mheap 维护着两个二叉搜索树,分别是 scav(垃圾回收后还的) 和 free(从 OS 申请的),mheap 会优先从 scav 中分配,无则从 free 中分配。若两者都没有空闲的,则会从 OS 中申请,之后再次遍历两颗二叉搜索树。
大对象申请过程
大对象不经过 mcache 和 mcentral,直接从 mheap 申请内存,分配在 mheap 的 arena 中
总结
Go 启动的时候会向操作系统申请一大块内存块(虚拟内存), 申请的内存会划分成三个部分 mspans,bitmap,arena
- arena 就是堆,存储的就是堆上初始化的对象
- bitmap 存储的是这个指针是否包含指针,还有一些 GC 信息,标识的是哪些地址保存了对象
- mspans 保存了 GO 内存管理的基本单元
内存分配器的组件主要有三个:线程缓存(mcache),中心缓存(mcentral) ,页堆(mheap)
- 线程缓存(mcache)启动时没有分配任何内存,向中心缓存(central) 申请缓存,并且每个线程只占用一个内存
- 中心缓存(mcentral) 带锁的全局缓存,是全部线程共享
- 页堆mheap 管理全部的中心缓存(mcentral) 。
总的来说,就是 Go 使用了内存多级管理, 降低锁的力度,每个线程有自己的本地内存,首先从线程的内存池进行分配,然后再从全局池进行申请。
