整体流程
函数细节:
mcache
Go 语言中的线程缓存,它会与线程上的处理器一一绑定,主要用来缓存用户程序申请的微小对象。每一个线程缓存都持有 67 * 2 个 runtime.mspan,这些内存管理单元都存储在结构体的 alloc 字段中:
1初始化
线程缓存mcache在刚刚被初始化时是不包含 runtime.mspan 的,只有当用户程序申请内存时才会从上一级组件获取新的 runtime.mspan 满足内存分配的需求
2 替换
当mache中某个mspan已经无可用的object可供申请时,runtime.mcache.refill 方法会为线程缓存获取一个指定spanclass的内存管理单元,新的mspan至少包含一个空闲object用以替换旧的mspan
func (c *mcache) refill(spc spanClass) {
s := c.alloc[spc]
s = mheap_.central[spc].mcentral.cacheSpan()
c.alloc[spc] = s
}
3 其他-微分配器
type mcache struct {
tiny uintptr//分配器起始地址
tinyoffset uintptr//下一个空闲地偏置
local_tinyallocs uintptr//已经分配的对象个数
}
用以分配小于16B且非指针对象。
mcentral
runtime.mcentral 是内存分配器的中心缓存,与线程缓存不同,访问中心缓存中可供全局多个线程申请,故内存管理单元需要使用互斥锁
type mcentral struct {
lock mutex
spanclass spanClass//类型,cenrtal[134],每个spanclass 确定0-66
nonempty mSpanList//含有空闲对象的列表
empty mSpanList//不含空闲对象的列表
nmalloc uint64//已分配对象个数
}
在mheap上一共存在这样的67*2个mcentral,除去指针和非指针区别,每个对应一个spanclass
1 初始化
该结构体在初始化时,两个链表都不包含任何内存,程序运行时会扩容结构体持有的两个链表,nmalloc 字段也记录了该结构体中分配的对象个数
2 与mcache的交互
线程缓存mcache会通过中心缓存的 runtime.mcentral.cacheSpan 方法获取新的内存管理单元,该方法的实现比较复杂,我们可以将其分成以下几个部分:
3 扩容
中心缓存mcentral的扩容方法 runtime.mcentral.grow 会根据预先计算的 class_to_allocnpages 和 class_to_size 获取待分配的页数以及跨度类并调用 runtime.mheap.alloc 获取新的 runtime.mspan 结构。
mheap
内存分配的核心结构体,Go 语言程序只会存在一个全局的结构,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。
页堆中包含一个长度为 134 的 runtime.mcentral 数组,其中 67 个为跨度类需要 scan 的中心缓存,另外的 67 个是 noscan 的中心缓存。
1 初始化
初始化所有mcentral,这些中心缓存会维护全局的内存管理单元,各个线程会通过中心缓存获取新的内存单元。
2 与mcentral交互
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)//为了阻止内存的大量占用和堆的增长,我们在分配对应页数的内存前需要先调用 runtime.mheap.reclaim 方法回收一部分内存
}
s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse)//从mheap申请一个mspan
})
...
return s
}
mheap的allocSpan主要流程如下(处理器的页缓存 runtime.pageCache 或者全局的页分配器 runtime.pageAlloc 两种途径从堆中申请内存):
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()//当前线程对应的处理器P
if pp != nil && npages < pageCachePages/4 {
c := &pp.pcache
base, scav = c.alloc(npages)//p的页缓存
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)//mheap全局堆区
if base == 0 {
h.grow(npages)//从系统申请固定页数大小的内存区域
base, scav = h.pages.alloc(npages)//重新从mheap获取
if base == 0 {
throw("grew heap, but no adequate free space found")
}
}
}
if s == nil {
s = h.allocMSpanLocked()
}
...
}
3 扩容
runtime.mheap.grow 方法会向操作系统申请更多的内存空间,传入的页数经过对齐可以得到期望的内存大小,该方法的执行过程分成以下几个部分:
其中sysAlloc 是从操作系统申请虚拟内存。
经典参考:https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/