go内存申请图解析

整体流程

go内存申请图解析_第1张图片

函数细节:

go内存申请图解析_第2张图片

mcache

Go 语言中的线程缓存,它会与线程上的处理器一一绑定,主要用来缓存用户程序申请的微小对象。每一个线程缓存都持有 67 * 2 个 runtime.mspan,这些内存管理单元都存储在结构体的 alloc 字段中:

go内存申请图解析_第3张图片

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

go内存申请图解析_第4张图片

1 初始化

该结构体在初始化时,两个链表都不包含任何内存,程序运行时会扩容结构体持有的两个链表,nmalloc 字段也记录了该结构体中分配的对象个数

2 与mcache的交互

线程缓存mcache会通过中心缓存的 runtime.mcentral.cacheSpan 方法获取新的内存管理单元,该方法的实现比较复杂,我们可以将其分成以下几个部分:

  1. 从有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元mspan(当mspan待回收时,直接准备使用故放入到empty列表,并调用回收逻辑;当mspan正回收时跳过;当mspan已经被回收时,直接使用故放回empty);
  2. 从没有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元mspan(与1类似);
  3. 调用 runtime.mcentral.grow 从堆中申请新的内存管理单元mspan;
  4. 更新内存管理单元mspan的 allocCache 等字段帮助快速分配内存;

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()
	}
	...
}
  1. 如果申请的内存比较小,获取申请内存的处理器并尝试调用 runtime.pageCache.alloc 获取内存区域的基地址和大小;
  2. 如果申请的内存比较大或者线程的页缓存中内存不足,会通过 runtime.pageAlloc.alloc 在页堆上申请内存;
  3. 如果发现页堆上的内存不足,会尝试通过 runtime.mheap.grow 进行扩容并重新调用 runtime.pageAlloc.alloc 申请内存;
    1. 如果申请到内存,意味着扩容成功;
    2. 如果没有申请到内存,意味着扩容失败,宿主机可能不存在空闲内存,运行时会直接中止当前程序;

3 扩容

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

  1. 通过传入的页数获取期望分配的内存空间大小以及内存的基地址;
  2. 如果 arena 区域没有足够的空间,调用 runtime.mheap.sysAlloc 从操作系统中申请更多的内存;
  3. 扩容 runtime.mheap 持有的 arena 区域并更新页分配器的元信息;
  4. 在某些场景下,调用 runtime.pageAlloc.scavenge 回收不再使用的空闲内存页;

其中sysAlloc 是从操作系统申请虚拟内存。

经典参考:https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/

你可能感兴趣的:(go)