内存分配器
程序中的数据和变量都会被分配到程序所在的虚拟内存中,内存空间包含两个重要的区域:栈区(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:notinheap
type 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 object
full [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 mutex
pages pageAlloc // page allocation data structure
sweepgen uint32 // sweep generation, see comment in mspan; written during STW
sweepdone uint32 // all spans are swept
sweepers 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 atomics
central [numSpanClasses]struct {
mcentral mcentral
pad [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 使用了内存多级管理, 降低锁的力度,每个线程有自己的本地内存,首先从线程的内存池进行分配,然后再从全局池进行申请。