内存分配管理
1.基本概念
Go构建了一套自主内存管理分配,没有使用传统的内存分配器malloc,器,原理类似于tcmalloc。为了方便管理内存,首先会向系统申请一大块内存,将内存切分成小块,通过内存分配的办法进行管理。
1.1 基本策略:
1>. 每次从操作系统申请一大块内存(比如 1MB),以减少系统调用。
2>. 将申请到的大块内存按照特定大小预先切分成小块,构成链表。
3>. 为对象分配内存时,只需从大小合适的链表提取一个小块即可。
4>. 回收对象内存时,将该小块内存重新归还到原链表,以便复用。
5>. 如闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销
1.2 基本结构:
1.2.1虚拟地址空间
1>. 使用arena 地址向操作系统申请内存,大小决定了可分配用户内存上限。
2>. 位图 bitmap 为每个对象提供 4bit 标记位,用以保存指针、GC 标记等信息。
3>. 创建 span 时,按页填充对应 spans 空间。在回收 object 时,只需将其地址按页对齐后就可找到所属 span。分配器还用此访问相邻 span,做合并操作。
arena
arena区域就是我们通常说的heap, go从heap分配的内存都在这个区域中.
bitmap
以bitmap区域的大小是 512GB / 指针大小(8 byte) / 4 = 16GB.
spans
spans区域中一个指针(8 byte)对应了arena区域中的一页(在go中一页=8KB).
512GB / 页大小(8KB) * 指针大小(8 byte) = 512MB.
GC Bitmap
bitmap区域: 涵盖了arena区域, 使用2 bit表示一个指针大小的内存
函数信息: 涵盖了函数的栈空间, 使用1 bit表示一个指针大小的内存 (位于stackmap.bytedata)
类型信息: 在分配对象时会复制到bitmap区域, 使用1 bit表示一个指针大小的内存 (位于_type.gcdata)
需要注意的是span结构本身的内存是从系统分配的, 上面提到的spans区域和bitmap区域都只是一个索引.
span中的元素大小是8 byte, span本身占1页也就是8K, 一共可以保存1024个对象.
首先从P的缓存(mcache)获取, 如果有缓存的span并且未满则使用, 这个步骤不需要锁
然后从全局缓存(mcentral)获取, 如果获取成功则设置到P, 这个步骤需要锁
最后从mheap获取, 获取后设置到全局缓存, 这个步骤需要锁
1.2.2 内存块种类
分配器将其管理的内存块分为两种:
span: 由多个地址连续的页(page)组成的大块内存。
object: 将 span 按特定特定切分成多个小块,每个小块可存储一个对象。
1.2.3 分配器组件
分配器由三种组件组成。
cache: 每个运行期间作线程都会绑定一个 cache,用于无锁 object 分配。
central: 为所有 cache 提供切分好的后备 span 资源。
heap: 管理闲置 span,需要时向操作系统申请新内存
1.3 分配流程
1>. 计算待分配对象对应规格(size class)。
2>. 从 cache.alloc 数组找到规格相同的 span。
3>. 从 span.freelist 链表提取可用object。
4>. 如 span.freelist 为空,从 central 获取新 span。
5>. 如 central.nonempty 为空,从 heap.free/freelarge 获取,并切分成 object 链表。
6>. 如 heap 没有大小合适的闲置 span,向操作系统申请新内存块。
1.4 释放流程
1>. 将标记为可回收 object 交还给所属 span.freelist。
2>. 该 span 被放回 central,可供任意 cache 重新获取使用。
3>. 如 span 已收回全部 object,则将其交还给 heap,以便重新切分复用。
4>. 定期扫描 heap里长时间闲置的 span,释放其占用内存
假如 cache1 获取一个 span 后,仅使用了一部分 object,那么剩余空间就可能会被浪费。而回收
操作将该 span 交还给 central 后,该 span 完全可以被 cache2、cacheN 获取使使用。此时,cache1已不再持有该 span,完全不会造成问题。
2. 源码分析
2.1 newobject
go从堆分配对象时会调用newobject函数, 这个函数的流程大致如下:
这三个阶段的详细结构如下图:
内存分配初始化工作:
1. 创建对象规格大小对照表。
2. 计算相关区域大小,并尝试从某个指定位置开始保留地址空间。
3. 在 heap里保存区域信息,包括起始位置和大小。
4. 初始化 heap 其他属性。
mheap内存管理示意图如下:
type mheap struct {
spans **mspan
spans_mapped uintptr
bitmap uintptr
bitmap_mapped uintptr
arena_start uintptr
arena_used uintptr
arena_end uintptr
arena_reserved bool
}
func mallocinit() {
//初始化规格对照表
initSizes()
//初始化head属性
mheap_.spans = (**mspan)(unsafe.Pointer(p1))
mheap_.bitmap = p1 + spansSize
mheap_.arena_start = p1 + (spansSize + bitmapSize)
mheap_.arena_used = mheap_.arena_start
mheap_.arena_end = p + pSize
mheap_.arena_reserved = reserved
//初始化heap
mHeap_Init(&mheap_, spansSize)
}
内存虚拟内存和物理内存的使用情况
1. 尽管初始化时预留了 544GB 的虚拟地址空间,但并没有分配内存。
2. 操作系统大多采取机会主义分配策略。申请内存时,仅承诺但不立即即分配物理内存。
3. 物理内存分配发生在写操作导致缺页异常调度时,且按页提供。
为对象分配内存需区分在栈还是堆上完成。通常情况下,编译器有责任尽可能使用寄存器
和栈来存储对象,这有助于提升性能,减少垃圾回收器压力。
newobject 具体是如何为对象分配内存的
type mcache struct {
// Allocator cache for tiny objects w/o pointers.
tiny unsafe.Pointer
tinyoffset uintptr
alloc [_NumSizeClasses]*mspan
}
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(uintptr(typ.size), typ, flags)
}
func mallocgc(size uintptr, typ *_type, flags uint32) unsafe.Pointer {
}
大对象直接从 heap 获取 span。
小对象从 cache.alloc[sizeclass].freelist 获取 object。
微小对象组合使用cache.tiny object。
内存回收
回收操作自然不会直接盯着单个对象,而是以 span 为基本单位。通过对
对 bitmap里的扫描标记,逐步将 object 收归原 span,最终上交 central 或 heap 复用。
func mSpan_Sweep(s *mspan, preserve bool) bool {
//为span空闲object设置标记,无需再次扫描
for link := s.freelist; link.ptr() != nil; link = link.ptr().next {
heapBitsForAddr(uintptr(link)).setMarkedNonAtomic()
}
}
内存释放
在入口函数main.mian里,会专门启动一个监控任务sysmon,每隔一段时间就会检查heap里的闲置内存块。
func sysmon() {
scavengelimit := int64(5 * 60 * 1e9)
for {
usleep(delay)
if lastscavenge+scavengelimit/2 < now {
mHeap_Scavenge(int32(nscavenge), uint64(now), uint64(scavengelimit))
lastscavenge = now
}
}
}
两种对象
整个进程内的对象可分为两类。其一,自然是从 arena 区域分配的用户
对象;另一种,则是运行时自身运行和管理所需,比如管理 arena 内存片段的 mspan,提
供无锁分配的 mcache 等等
固定分配器
type fixalloc struct {
size uintptr
first unsafe.Pointer
arg unsafe.Pointer
list *mlink
chunk *byte
nchunk uint32
inuse uintptr
}
在运行时在初始化 heap 时,一共构建了 4 种固定分配器。
func mHeap_Init(h *mheap, spans_size uintptr) {
fixAlloc_Init(&h.spanalloc, unsafe.Sizeof(mspan{}), recordspan, ...)
fixAlloc_Init(&h.cachealloc, unsafe.Sizeof(mcache{}), nil, nil, ...)
fixAlloc_Init(&h.specialfinalizeralloc, unsafe.Sizeof(specialfinalizer{}), nil, ...)
fixAlloc_Init(&h.specialprofilealloc, unsafe.Sizeof(specialprofile{}), nil, ...)
}
总结
Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。
Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域
arena区域按页划分成一个个小块
span管理一个或多个页
mcentral管理多个span供线程申请使用
mcache作为线程私有资源,资源来源于mcentral
参考资料:https://my.oschina.net/renhc/blog/2236782?spm=a2c4e.11153940.blogcont652551.13.3e3f24754ybVI2