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


表 7-3 平台与页堆大小的关系
本节将介绍页堆的初始化、内存分配以及内存管理单元分配的过程,这些过程能够帮助我们理解全局变量页堆与其他组件的关系以及它管理内存的方式 。
初始化堆区的初始化会使用 runtime.mheap.init 方法,我们能看到该方法初始化了非常多的结构体和字段,不过其中初始化的两类变量比较重要:

  1. spanalloc、cachealloc 以及 arenaHintAlloc 等 runtime.fixalloc 类型的空闲链表分配器;
  2. central 切片中 runtime.mcentral 类型的中心缓存;
func (h *mheap) init() { h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys) h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys) h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys) h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys) h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys) h.spanalloc.zero = false for i := range h.central {h.central[i].mcentral.init(spanClass(i)) } h.pages.init(&h.lock, &memstats.gc_sys)}堆中初始化的多个空闲链表分配器与我们在设计原理一节中提到的分配器没有太多区别,当我们调用 runtime.fixalloc.init 初始化分配器时,需要传入带初始化的结构体大小等信息,这会帮助分配器分割待分配的内存,该分配器提供了以下两个用于分配和释放内存的方法:
  1. runtime.fixalloc.alloc — 获取下一个空闲的内存空间;
  2. runtime.fixalloc.free — 释放指针指向的内存空间;
除了这些空闲链表分配器之外,我们还会在该方法中初始化所有的中心缓存,这些中心缓存会维护全局的内存管理单元,各个线程会通过中心缓存获取新的内存单元 。
内存管理单元runtime.mheap 是内存分配器中的核心组件,运行时会通过它的 runtime.mheap.alloc 方法在系统栈中获取新的 runtime.mspan:
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan { var s *mspan systemstack(func() {if h.sweepdone == 0 {h.reclaim(npages)}s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse) }) ... return s}为了阻止内存的大量占用和堆的增长,我们在分配对应页数的内存前需要先调用 runtime.mheap.reclaim 方法回收一部分内存,接下来我们将通过 runtime.mheap.allocSpan 分配新的内存管理单元,我们会将该方法的执行过程拆分成两个部分:
  1. 从堆上分配新的内存页和内存管理单元 runtime.mspan;
  2. 初始化内存管理单元并将其加入 runtime.mheap 持有内存单元列表;
首先我们需要在堆上申请 npages 数量的内存页并初始化 runtime.mspan:
func (h *mheap) allocSpan(npages uintptr, manual bool, spanclass spanClass, sysStat *uint64) (s *mspan) { gp := getg() base, scav := uintptr(0), uintptr(0) pp := gp.m.p.ptr() if pp != nil && npages < pageCachePages/4 {c := &pp.pcachebase, scav = c.alloc(npages)if base != 0 {s = h.tryAllocMSpan()if s != nil && gcBlackenEnabled == 0 && (manual || spanclass.sizeclass() != 0) {goto HaveSpan}} } if base == 0 {base, scav = h.pages.alloc(npages)if base == 0 {h.grow(npages)base, scav = h.pages.alloc(npages)if base == 0 {throw("grew heap, but no adequate free space found")}} } if s == nil {s = h.allocMSpanLocked() } ...}上述方法会通过处理器的页缓存 runtime.pageCache 或者全局的页分配器runtime.pageAlloc 两种途径从堆中申请内存:
  1. 如果申请的内存比较小,获取申请内存的处理器并尝试调用 runtime.pageCache.alloc 获取内存区域的基地址和大小;
  2. 如果申请的内存比较大或者线程的页缓存中内存不足,会通过 runtime.pageAlloc.alloc在页堆上申请内存;
  3. 如果发现页堆上的内存不足,会尝试通过 runtime.mheap.grow 进行扩容并重新调用 runtime.pageAlloc.alloc 申请内存;
    1. 如果申请到内存,意味着扩容成功;
    2. 如果没有申请到内存,意味着扩容失败,宿主机可能不存在空闲内存,运行时会直接中止当前程序;
无论通过哪种方式获得内存页,我们都会在该函数中分配新的 runtime.mspan 结构体;该方法的剩余部分会通过页数、内存空间以及跨度类等参数初始化它的多个字段:
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan { ...HaveSpan: s.init(base, npages) ... s.freeindex = 0 s.allocCache = ^uint64(0) s.gcmarkBits = newMarkBits(s.nelems) s.allocBits = newAllocBits(s.nelems) h.setSpans(s.base(), npages, s) return s}在上述代码中,我们通过调用 runtime.mspan.init 方法以及设置参数初始化刚刚分配的 runtime.mspan 结构并通过 runtime.mheaps.setSpans 方法建立页堆与内存单元的联系 。


推荐阅读