go内存管理浅析
/ / 点击引言
Go的内存分配器基于TCMalloc(Thread Local Malloc),并在TCMalloc基础上增加了自动回收机制(TCMalloc只是增加内存申请速度,用户需手动释放空间)。
内存管理中的重要组件
在开始介绍之前,主要介绍以下Go内存分配中几个重要的组件:
- mheap,管理整个Go程序使用的堆空间,当程序内存不够需要申请更多内存时就是由mheap向os申请资源。操作单位是页,页大小是8KB
- mcentral,管理协程需要使用的内存,mcentral具有不同的规格属性,每个mcentral都管理着自身规格的内存。管理单元是mspan,mspan的规格(每个指定规格的mspan其上只能存放指定规格大小的数据)详情见源码(go/src/runtime/sizeclasses.go)。
- mcache,协程模型中
P上的缓存内存。主要供协程使用,由于P上每次最多只可能有一个G在running,因此对P上的缓存操作无需加锁,增加了内存申请与释放的速度。缓存单位是mspan。三者协作: mcache是P私有且无锁的内存,当协程需要内存数据时,直接从mcache上找到指定规格的mspan,并将数据存入mspan。当mspan为空(初始为空)或已满时,向mcentral申请该规格的mspan,mcentral根据自身情况,有空闲则返回,无空间则从mheap中申请,mheap则根据自身是否由空间来决定返回自身空闲空间或者向OS申请资源。
mheap
主要成员:
- arenas [][]heapArena
- allspans []mspan
- central []mcentral
arenas
类型为
heapArena的二元数组,主要数据结构为:
1 | type heapArena struct { |
spans成员变量保存了该heapArena中的mspan,该变量长度为heapArena大小/areaPageSize。
某个page未被mspan使用时,它的值为nil,被使用时,则该page对应的索引值即为被使用的mspan的指针(当一个mspan使用多个page时,多个page索引对应值均为该mspan指针)。
以linux 64位为例,areaPageSize是8KB,arenaSize是64MB、所以该变量长度为 64MB/8KB=8K=8096pageInUse成员变量标记了该arena中哪些page被使用,哪些未被使用,使用位图标记,所以数组长度=pagesPerArena/8=heapArena大小/areaPageSize/8。
以linux 64位为例,areaPageSize是8KB,arenaSize是64MB、所以该变量长度为 64MB/8KB/8=1K=1024bitmap位图标记单位是words(arena中所有的words),又因为bitmap中每2个bit表示一个words的2个属性(pointer/scalar),所以bitmap数组长度应是heapArenaBytes/(len(words)*8/2)即arena字节数/words字节数/8*2。
以linux 64位为例,arenaSize是64MB,则bitmap长度是 64MB/8B/8 * 2=2M 即 2 * 1024 * 1024
allspans
顾名思义,存放着所有的mspan
central
heap.central变量是一个数组,数组长度136(0/1表示大对象),表示67种规格的mcentral对象(数组一半表示带指针,一半表示不带指针,区分主要是为了gc方便)
mcentral
1 | type mcentral struct{ |
mcentral成员变量
spanclass表明了该mcentral对象的规格partial表示仍有空间可使用的mspan对象full表示无空间可用的mspan对象
mcentral核心方法
- cacheSpan(): 分配一个
mspan对象给mcache(P上管理mspan的对象)用。
分配过程找可用的mspan顺序:
① 从partial变量中sweep过的列表里找到一个有空闲的,如果有使用该mspan如果无则继续。
② 从partial变量中未sweep过的列表里找到一个mspan并清理它,如果有使用该mspan如果无则继续。
③ 从full变量中未sweep过的列表中找到一个mspan并清理它,清理后可用则使用它,如果无则继续。
④ 以上三个过程都未找到可用mspan则调用mcentral.grow()方法,申请空间- uncacheSpan(): 从
mcache中拿走一个mspan,根据其gc阶段放到对应的spanSet中- grow(): 从
heap分配一个新的空的mspan并按照mcentral.spanclass规格将其初始化。核心调用链-> mheap_.alloc -> mheap_.allocSpan
mcache
小对象线程(GMP模型中的P)私有的的cache,因为是线程独享的所以访问时无需加锁。
mcache对象自身不在堆上(资源未托管给go的GC做清理),因此该对象中涉及到堆上的指针时需要特殊处理。
mcache主要成员变量:
1 | type mcache struct { |
mcache中主要成员方法
- nextFree(spc spanclass) 从当前
mcache中找到规格为spc的有空闲的mspan,如果没有返回空(上层会调用refill方法)- refill(spc spanclass) 将当前
mcache中规格为spc的mspan放到对应mcentral中,并从中拿到一个可用的mspan给mcache- allocLarge() 大对象分配时调用,直接通过
mheap_.alloc从堆中分配- releaseAll() 释放所有
mspan对象给mheap.central,主要是在P终结时调用
内存管理中的重要对象
- mheap_,mheap的实例对象。
- mheap_.mcentral[i] ,是具体mcentral的一个实例,mheap_.mcentral长度为136(67种规格×2是否带指针+2表示大对象)。mheap_.mcentral[i]里负责存放全局的i规格内存和管理他们的申请与释放。
- p.mcache[i],
P上缓存内存示例,其也是一个数组,长度也为134。当p.mcache[i]为空或满时,需要申请内存就向mheap_.mcentral[i]申请。- mspan,mcache、mcentral管理的内存单元。每次申请或者清理都是以mspan大小的单位,mspan具有规格属性,不同规格的mspan其大小及存放该规格数据的数量也都不尽相同。
- mheap_.arenas[i][j], 数据类型为heapArena,该对象主要属性:bitmap[],spans[],pageInUse,pageMarks,zeroedBase,网上很多讲go内存管理都会提到,go内存分为3部分,spans,bitmap,arena其中,arena指的就是mheap_.arenas中描述的所有内存大小,bitmap指的便是这里的属性bitmap[],spans指的便是这里的spans[],bitmap[]是字节数组,每个字节有8bit,可以用来表示4字节的内存使用情况以及gc标志,spans[]是指针数组,每个指针大小为8byte,指向一个mspan。关于说堆最大512G的说法是错误的,在linux 的go1.14中,arenas[i][j].bitmap[]大小是2MB,2MB一共是2 * 1024 * 1024 * 8 bit可以表示 8 * 1024 * 1024个地址位信息即 8M * 8byte = 64MB信息,arena数组大小是 1 * 4M,所以理论上,arenas堆空间最大内存为64MB * 4M = 256TB,bitmap占空间大小为 2MB * 4M = 8TB;spans占内存大小为 8K(数组长度) * 8B(数组指针元素8B) = 64KB(spans可表示内存:8K * 8KB(页大小)=64MB,64MB * 4M(arenas数组大小)=256TB 这与通过bitmap计算出的结果是一致的)
架构简图
注:该图基于Go1.14画的,本文更新后很多内容是基于新版本Go1.16的。其中
mcentral成员变量emptynoempty对应到新版本是:partialfull
堆对象空间分配
堆对象分配的主要逻辑在
runtime/malloc.go中的mallocgc()函数
微小(tiny)对象分配 (0,16byte]
按照代码中的逻辑,当待分配空间的对象大小<=16byte且没有指针引用时,使用微小对象分配的方法,将其放到mcache.alloc[5]的mspan中,这个mspan的规格是16byte。
1 | // Tiny allocator. |
小对象分配 (16byte,32kb)
小对象内存分配,比微对象少了从 mcache.tiny上快速分配空间的逻辑,同微对象后边的分配空间逻辑大致相同
1 | // 确定sizeclass |
大对象分配 (32kb,∞)
大对象内存分配直接从heap申请资源
1 | var s *mspan |
