Go 内存分配器的设计与实现( 八 )


扩容runtime.mheap.grow 方法会向操作系统申请更多的内存空间,传入的页数经过对齐可以得到期望的内存大小,我们可以将该方法的执行过程分成以下几个部分:

  1. 通过传入的页数获取期望分配的内存空间大小以及内存的基地址;
  2. 如果 arena 区域没有足够的空间,调用 runtime.mheap.sysAlloc 从操作系统中申请更多的内存;
  3. 扩容 runtime.mheap 持有的 arena 区域并更新页分配器的元信息;
  4. 在某些场景下,调用 runtime.pageAlloc.scavenge 回收不再使用的空闲内存页;
在页堆扩容的过程中,runtime.mheap.sysAlloc 是页堆用来申请虚拟内存的方法,我们会分几部分介绍该方法的实现 。首先,该方法会尝试在预保留的区域申请内存:
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { n = alignUp(n, heapArenaBytes) v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys) if v != nil {size = ngoto mapped } ...}上述代码会调用线性分配器的 runtime.linearAlloc.alloc 方法在预先保留的内存中申请一块可以使用的空间 。如果没有可用的空间,我们会根据页堆的 arenaHints 在目标地址上尝试扩容:
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { ... for h.arenaHints != nil {hint := h.arenaHintsp := hint.addrv = sysReserve(unsafe.Pointer(p), n)if p == uintptr(v) {hint.addr = psize = nbreak}h.arenaHints = hint.nexth.arenaHintAlloc.free(unsafe.Pointer(hint)) } ... sysMap(v, size, &memstats.heap_sys) ...}runtime.sysReserve 和 runtime.sysMap 是上述代码的核心部分,它们会从操作系统中申请内存并将内存转换至 Prepared 状态 。
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { ...mapped: for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {l2 := h.arenas[ri.l1()]r := (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))...h.allArenas = h.allArenas[:len(h.allArenas)+1]h.allArenas[len(h.allArenas)-1] = riatomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r)) } return}runtime.mheap.sysAlloc 方法在最后会初始化一个新的 runtime.heapArena 结构体来管理刚刚申请的内存空间,该结构体会被加入页堆的二维矩阵中 。
内存分配堆上所有的对象都会通过调用 runtime.newobject 函数分配内存,该函数会调用 runtime.mallocgc 分配指定大小的内存空间,这也是用户程序向堆上申请内存空间的必经函数:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { mp := acquirem() mp.mallocing = 1 c := gomcache() var x unsafe.Pointer noscan := typ == nil || typ.ptrdata =https://www.isolves.com/it/cxkf/yy/go/2020-06-07/= 0 if size <= maxSmallSize {if noscan && size < maxTinySize {// 微对象分配} else {// 小对象分配} } else {// 大对象分配 } publicationBarrier() mp.mallocing = 0 releasem(mp) return x}上述代码使用 runtime.gomcache 获取了线程缓存并通过类型判断类型是否为指针类型 。我们从这个代码片段可以看出 runtime.mallocgc 会根据对象的大小执行不同的分配逻辑,在前面的章节也曾经介绍过运行时根据对象大小将它们分成微对象、小对象和大对象,这里会根据大小选择不同的分配逻辑:
Go 内存分配器的设计与实现

文章插图
 
allocator-and-memory-size
图 7-19 三种对象
  • 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
  • 大对象 (32KB, +∞) — 直接在堆上分配内存;
我们会依次介绍运行时分配微对象、小对象和大对象的过程,梳理内存分配的核心执行流程 。
微对象Go 语言运行时将小于 16 字节的对象划分为微对象,它会使用线程缓存上的微分配器提高微对象分配的性能,我们主要使用它来分配较小的字符串以及逃逸的临时变量 。微分配器可以将多个较小的内存分配请求合入同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收 。
微分配器管理的对象不可以是指针类型,管理多个对象的内存块大小 maxTinySize 是可以调整的,在默认情况下,内存块的大小为 16 字节 。maxTinySize 的值越大,组合多个对象的可能性就越高,内存浪费也就越严重;maxTinySize 越小,内存浪费就会越少,不过无论如何调整,8 的倍数都是一个很好的选择 。
Go 内存分配器的设计与实现

文章插图
 


推荐阅读