- 从有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元;
- 从没有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元;
- 调用 runtime.mcentral.grow 从堆中申请新的内存管理单元;
- 更新内存管理单元的 allocCache 等字段帮助快速分配内存;
- 当内存单元等待回收时,将其插入 empty 链表、调用 runtime.mspan.sweep 清理该单元并返回;
- 当内存单元正在被后台回收时,跳过该内存单元;
- 当内存单元已经被回收时,将内存单元插入 empty 链表并返回;
func (c *mcentral) cacheSpan() *mspan { sg := mheap_.sweepgenretry: var s *mspan for s = c.nonempty.first; s != nil; s = s.next {if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) { // 等待回收c.nonempty.remove(s)c.empty.insertBack(s)s.sweep(true)goto havespan}if s.sweepgen == sg-1 { // 正在回收continue}c.nonempty.remove(s) // 已经回收c.empty.insertBack(s)goto havespan } ...}
如果中心缓存没有在 nonempty 中找到可用的内存管理单元,就会继续遍历其持有的 empty链表,我们在这里的处理与包含空闲对象的链表几乎完全相同 。当找到需要回收的内存单元时,我们也会触发 runtime.mspan.sweep 进行清理,如果清理后的内存单元仍然不包含空闲对象,就会重新执行相应的代码:func (c *mcentral) cacheSpan() *mspan { ... for s = c.empty.first; s != nil; s = s.next {if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {c.empty.remove(s)s.sweep(true)freeIndex := s.nextFreeIndex()if freeIndex != s.nelems {s.freeindex = freeIndexgoto havespan}goto retry // 不包含空闲对象}if s.sweepgen == sg-1 {continue}break } ...}
如果 runtime.mcentral 在两个链表中都没有找到可用的内存单元,它会调用 runtime.mcentral.grow 触发扩容操作从堆中申请新的内存:func (c *mcentral) cacheSpan() *mspan { ... s = c.grow() if s == nil {return nil } c.empty.insertBack(s)havespan: n := int(s.nelems) - int(s.allocCount) atomic.Xadd64(&c.nmalloc, int64(n)) if gcBlackenEnabled != 0 {gcController.revise() } freeByteBase := s.freeindex &^ (64 - 1) whichByte := freeByteBase / 8 s.refillAllocCache(whichByte) s.allocCache >>= s.freeindex % 64 return s}
无论通过哪种方法获取到了内存单元,该方法的最后都会对内存单元的 allocBits 和 allocCache 等字段进行更新,让运行时在分配内存时能够快速找到空闲的对象 。扩容中心缓存的扩容方法 runtime.mcentral.grow 会根据预先计算的 class_to_allocnpages 和class_to_size 获取待分配的页数以及跨度类并调用 runtime.mheap.alloc 获取新的 runtime.mspan 结构:
func (c *mcentral) grow() *mspan { npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) size := uintptr(class_to_size[c.spanclass.sizeclass()]) s := mheap_.alloc(npages, c.spanclass, true) if s == nil {return nil } n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2 s.limit = s.base() + size*n heapBitsForAddr(s.base()).initSpan(s) return s}
获取了 runtime.mspan 之后,我们会在上述方法中初始化 limit 字段并清除该结构在堆上对应的位图 。页堆runtime.mheap 是内存分配的核心结构体,Go 语言程序只会存在一个全局的结构,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段 。
页堆中包含一个长度为 134 的 runtime.mcentral 数组,其中 67 个为跨度类需要 scan 的中心缓存,另外的 67 个是 noscan 的中心缓存:
文章插图
mheap-and-mcentrals
图 7-17 页堆与中心缓存列表
我们在设计原理一节中已经介绍过 Go 语言所有的内存空间都由如下所示的二维矩阵 runtime.heapArena 管理的,这个二维矩阵管理的内存可以是不连续的:
文章插图
mheap-and-memories
图 7-18 页堆管理的内存区域
在除了 Windows 以外的 64 位操作系统中,每一个 runtime.heapArena 都会管理 64MB 的内存空间,如下所示的表格展示了不同平台上 Go 语言程序管理的堆区大小以及 runtime.heapArena 占用的内存空间:
平台地址位数Arena 大小一维大小二维大小*/64-bit4864MB14M (32MB)windows/64-bit484MB641M (8MB)*/32-bit324MB11024 (4KB)*/mips(le)314MB1512 (2KB)
推荐阅读
- 茶艺享受的是时间,茶艺根据不同的原则和方法的分类
- 茶油的营养价值,长期吃茶油的坏处
- 茶具的选购之配套用具,茶具的奇思妙想
- 科学管理Linux系统中的组与组成员
- 大红袍的产地,名茶大红袍的产地是哪
- svchost占用内存过高是怎么回事
- 基于MEC的边缘CDN业务调度方案及测试分析
- 茶香面包的做法,红茶面包棒的做法
- JavaScript的Array.flat函数深入探讨
- 买电脑、DIY电脑,你必须了解的避坑技能