本文是Go内存管理的下篇,主要围绕内存分配源码分析其工作原理,分析的版本是Go1.14版。在Go1.11之后内存分配做了不少改动,网上的很多资料分析的都是旧版的代码,跟当前最新的版本差异比较大。Go内存分配涉及的代码很多,代码在runtime包下的malloc.go、mcache.go、mcentral.go、mgc.go、mheap.go、mpagealloc.go、mpagealloc_64bit.go、mpagecache.go、mpallocbits.go、mbitmap.go,阅读需要花费不少时间,本文是小编阅读完上述代码做的一个分析总结输出。在阅读本文之前,建议读者先看Go内存管理-上篇,上篇总结了TCMalloc的工作原理,Go的内存管理基于的是TCMalloc,所以看完上篇之后再来看本文更容易理解。
在开始介绍Go内存管理组件和具体对象是如何进行分配的之前,我们先来了解mspan、heapArena、对象大小几个基本概念,方便后文的理解。
mspan是golang内存管理的基本单位,每个span管理指定规格(以page为单位)的内存块,具体规格以page为单位,page的概念可以看Go内存管理-上篇文章,内存池分配出不同规格的内存块就是通过span体现出来的,应用程序创建对象就是通过找到对应规格的span来存储的.通过下面mspan结构定义可以看到,它主要包含startAddr和npages两个核心参数,startAddr记录了mspan管理的页面对应的内存首地址,npages表明了该mspan管理了多少个页面。
type mspan struct {
// 链表中下一个mspan
next *mspan
// 链表中上一个mspan
prev *mspan
list *mSpanList // For debugging. TODO: Remove.
// 该mspan的起始地址
startAddr uintptr
// mspan中包含多少页
npages uintptr
...
}
Go中的堆被表示成由多个arena组成,每个arena在64位机器上为64MB,且起始地址与arena的大小对齐,所有的arena覆盖了整个Go堆的地址空间。运行时使用二维的runtime.heapArena数组管理所有的内存,每个单元都会管理64MB的内存空间。heapArena中bitmap用于标识当前arena区域中的那些地址保存了对象,位图中的每个bit都会表示堆区中的字节是否空闲,spans数存储了mspan的指针,每个内存单元会管理几页的内存空间,每页大小为8KB。
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
// pagesPerArena=8192,一个mspan与一个page 一一对应, 一个page为8k
// 所以一个heapArena管理的大小为8192*8k=64M
spans [pagesPerArena]*mspan
// pageInUse [1024]uint8 即1024个Byte,每个bit标识一个page是否被使用
pageInUse [pagesPerArena / 8]uint8
// pageMarks [1024]uint8,即1024个Byte,每个bit标识一个page中是否有对象被marked
pageMarks [pagesPerArena / 8]uint8
zeroedBase uintptr
}
Go中的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种。微对象大小在(0,16B),小对象的大小在[16B,32KB],大对象大小在(32KB,+∞)。大多数对象的大小都在32KB以下,而申请的内存大小影响Go运行时分配内存的过程和开销,分别处理大对象和小对象有利于提高内存分配器的性能。
内存分配由内存分配器完成,Go中的内存分配器组件有mheap、mcentral和mcache,可以看到Go中的组件与TCMalloc是一致的,建议看完Go内存管理-上篇, 在看下面的内容,方便理解。
每个P(GPM中的P)都会有一个mcache。mcache保存了本地可用的mspan对象,这样相同P下的goroutine在申请内存(小对象和微对象)的时候,直接从本地的mcache中分配。同一个P一次只能运行一个goroutine,所以相同P下的goroutine在申请内存的时候不存在竞争的情况,不用加锁处理。
mcache结构定义如下:
type mcache struct {
// 分配一定字节后触发堆采样
next_sample uintptr
// 分配的可扫描堆字节数,GC的时候会用
local_scan uintptr
// 申请微对象(<16B)的起始地址
tiny uintptr
//从起始地址tiny开始的偏移量
tinyoffset uintptr
//tiny对象分配的数量
local_tinyallocs uintptr
// 分配的mspan list,其中numSpanClasses=134
alloc [numSpanClasses]*mspan
//栈缓存
stackcache [_NumStackOrders]stackfreelist
// 大对象释放字节数
local_largefree uintptr
// 释放的大对象数量
local_nlargefree uintptr
// 每种规格小对象释放的个数
local_nsmallfree [_NumSizeClasses]uintptr
// 扫描计数
flushGen uint32
}
type p struct {
...
mcache *mcache
...
}
mcache结构重点关注alloc字段,这是一个*mspan类型的数组,数组大小numSpanClasses的值为134,因为SpanClasses一共有67种,为了满足指针对象和非指针对象,这里为每种规格的span同时准备scan和noscan两个,因此一共有134个mspan缓存链表,分别用于存储指针对象和非指针对象,这样对非指针对象扫描的时候不需要继续扫描它是否引用其他对象,GC扫描对象的时候对于noscan的span可以不去查看bitmap区域来标记子对象, 这样可以大幅提升标记的效率。
mcentral与TCMalloc中的CentralCache类似,是所有线程共享的缓存,访问时需要加锁。它按spanclass级别对span分类,然后串联成链表,当mcache的某个级别span的内存被分配光时,它会向mcentral申请1个当前级别的span。
mcentral结构定义如下:
type mcentral struct {
lock mutex
// span大小规格,当前有67种
spanclass spanClass
// nonmepty表示还没有空object,可以继续从这里获取到有空object的span
nonempty mSpanList
// empty表示已经没有空object了,所有的span都已经分配出去了,或者划分给了mcache
empty mSpanList
// 分配的对象数
nmalloc uint64
}
mcentral维护了一种spanClass的span对象链表,它有两个mSpanList链表,分别是nonempty和empty,nonempty链表中span表示还有空闲的object,empty表示它里面的span已经没有空闲object了,都已分配出去了。
mheap与TCMalloc中的PageHeap类似,它是堆内存的抽象,把从OS申请出的内存页组织成Span,并保存起来。每个Go程序使用一个mheap的全局对象mheap_来管理堆内存。当mcentral的Span不够用时会向mheap申请内存,而mheap的Span不够用时会向OS申请内存。mheap主要用于大对象的内存分配,以及管理未切割的mspan,用于给mcentral切割成小对象。mheap结构定义如下:
type mheap struct {
lock mutex
// page分配器
pages pageAlloc
// GC时清扫标记值
sweepgen uint32
// 标记清扫是否完成
sweepdone uint32
// 有效的清扫调用数
sweepers uint32
// 保存了所有的的mspan指针对象的数组
allspans []*mspan
sweepSpans [2]gcSweepBuf
// 有多少页正在被使用
pagesInUse uint64
// 扫描的页面数量
pagesSwept uint64
// 用做扫描比例的初始基点
pagesSweptBasis uint64
// 用做扫描比例的初始处于存活状态的初始基点
sweepHeapLiveBasis uint64
//扫描比
sweepPagesPerByte float64
scavengeGoal uint64
reclaimIndex uint64
reclaimCredit uintptr
// 大对象分配的字节数
largealloc uint64
// 大对象分配的数量
nlargealloc uint64
// 大对象释放的字节数
largefree uint64
// 大对象释放的数量
nlargefree uint64
// 小对象释放的数量
nsmallfree [_NumSizeClasses]uint64
//arenas数组集合,管理各个heapArena
// arenas是二级数组,数组中的元素是*heapArena,在mac下第一维大小为1
// 第二维大小为1<<22 = 1024*1024*4
// 一个heapArena管理的内存大小为64M,所以arenas管理的内存为 1024*1024*4*64M=256T
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
// heapArenaAlloc内存分配器,32位系统上会用这个分配
heapArenaAlloc linearAlloc
// arena内存分配器,32系统上会用这个分配arena
arena linearAlloc
//所有arena序号集合,可以根据arenaIdx算出对应arenas中的哪一个heapArena
allArenas []arenaIdx
//扫描周期开始时allArenas的一个快照
sweepArenas []arenaIdx
curArena struct {
base, end uintptr
}
_ uint32
// 各个规格的mcentral集合,numSpanClasses=134
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
// span*的内存分配器,只是分配空结构
spanalloc fixalloc
// mcache*的内存分配器
cachealloc fixalloc
// specialfinalizer*的内存分配器
specialfinalizeralloc fixalloc
// specialprofile*的内存分配器
specialprofilealloc fixalloc
speciallock mutex
// arenaHintAlloc的内存分配器
arenaHintAlloc fixalloc
unused *specialfinalizer
}
分配对象的大小超过32KB视为大对象分配,大对象的分配流程与小对象和tiny对象的分配是不一样的。大对象的分配直接走heap进行分配。
无论是大对象还是小对象的分配统一入口都是newobject,newobject内部直接调用了mallocgc函数,mallocgc函数根据分配对象的大小做不同的处理逻辑,下面的代码只保留了大对象分配的核心逻辑,去掉了干扰项,方便我们理解大对象分配的主要流程。可以看到,对于大对象的处理,走的是largeAlloc函数,该函数传入3个参数:要分配的内存大小、是否需要对分配的内存清0、分配的对象是否含有指针。函数的返回值是一个mspan对象,而mallocgc函数要返回的是一个内存起始地址,mspan的base方法返回的就是mspan管理的pages的首地址,正是我们需要的,所以x = unsafe.Pointer(s.base())中的x是将要分配对象的起始地址。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
// 小对象,<=32KB
if size <= maxSmallSize {
// 16B,且不含指针
if noscan && size < maxTinySize {
...
} else { //含有指针 或要分配的内存大于等于16B
...
}
} else { // >32KB对象的内存分配,直接走heap分配
var s *mspan
shouldhelpgc = true
// 切换到系统栈上执行,传入参数为要分配的内存大小,是否需要清0,申请的对象是否包含指针
// 返回分配到的mspan
systemstack(func() {
// 调用largeAlloc分配一个大对象的mspan
s = largeAlloc(size, needzero, noscan)
})
// 设置mspan的一些辅助字段,mspan存储大对象只放一个,所以s.allocCount赋值为1
// freeindex表示下一个空闲的对象是mspan中第几个对象,freeindex从0开始,因为已分配
// 出去了一个对象,下一个空闲对象index为1,所以这里s.freeindex赋值为1,实际上mspan已分配完了
// 因为大对象,一个mspan只放一个object
s.freeindex = 1
s.allocCount = 1
x = unsafe.Pointer(s.base())
size = s.elemsize
}
...
return x
}
largeAlloc函数处理大对象的分配,根据分配对象的大小,转换成要分配多少个page,一个page大小为8KB, 如果分配的大小不是8KB的整数倍,向上取整,即分配的页数对应管理的内存不小于size. 例如size=32KB+1,则需要分配(32KB+1)/8KB=5个page。然后创建spanClass, 对应大对象来说,spanClass为0或1,如果待分配的对象中含有指针,spanClass为1,不含指针,spanClass为0.最后调用mheap_.alloc分配一个mspan对象。
// largeAlloc用于大对象的分配,>32KB对象内存分配为大对象
func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {
// 分配的内存过大溢出了,抛出异常
if size+_PageSize < size {
throw("out of memory")
}
// 计算需要多少个页
npages := size >> _PageShift
// size&_PageMask等价于size%_PageSize, _PageSize=_PageMask+1
// 即不是_PageSize(8KB)的整数倍,npages+1个,确保分配的大小不能小于size
if size&_PageMask != 0 {
npages++
}
// 先做一些sweep工作
deductSweepCredit(npages*_PageSize, npages)
// makeSpanClass将sizeclass转换成spanClass, 大对象的sizeclass为0
// 大对象的spanClass不是0就是1,如果分配的对象中含有指针,spanClass就是1,不含指针,spanClass就是0
s := mheap_.alloc(npages, makeSpanClass(0, noscan), needzero)
if s == nil {
throw("out of memory")
}
// limit保存了分配内存的上限地址位置
s.limit = s.base() + size
// bitmap设置,用于GC的
heapBitsForAddr(s.base()).initSpan(s)
return s
}
alloc方法分配一个新的mspan,真正分配是h.allocSpan做的,它本身除了的逻辑不多,主要是在进行h.allocSpan前进对不用的页面进行清扫回收,对分配到的内存根据是否需要清0做一些清理操作。
// alloc分配一个新的mspan,mspan的对象规格为入参指定的规格,管理的页面数为npages
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan {
var s *mspan
systemstack(func() {
// 为了防止堆过度增长,在分配n个页面之前,我们需要清除并回收至少n个页面
if h.sweepdone == 0 {
h.reclaim(npages)
}
// 调用allocSpan分配一个mspan
s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse)
})
if s != nil {
if needzero && s.needzero != 0 {
// 对分配到的内存进行清理操作 memclrNoHeapPointers(unsafe.Pointer(s.base()), s.npages<<_PageShift)
}
s.needzero = 0
}
return s
}
allocSpan根据要分配的页面数目、分配对象的规格、是否是手动分配参数分配一个mspan对象。sysStat是统计内存使用情况的参数,跟本文要分析的内容关系不大,这里不用关心。如果是手动分配,即manual为true,需要调用者负责与其使用span相关的信息记录维护,如果是manual为false,自动在span中更新了堆的使用信息。总结起来,allocSpan分配一个span对象要完成三步操作:
func (h *mheap) allocSpan(npages uintptr, manual bool, spanclass spanClass, sysStat *uint64) (s *mspan) {
// Function-global state.
gp := getg()
base, scav := uintptr(0), uintptr(0)
// 获取当前的P中的pageCache, pageCachePages为64
pp := gp.m.p.ptr()
// 如果分配的page数小于16个,尝试从P的pageCache中分配
if pp != nil && npages < pageCachePages/4 {
c := &pp.pcache
// pageCache每个P有1个,它表示64*8KB=512KB大小的一块内存空间,在该内存空间中可能有0个或多个空闲页面。
// c中cache全为0,表示所有所有的页面都已分配出去了,没有空闲的页面了
if c.empty() {
lock(&h.lock)
// 申请分配一个新的pageCache给P
*c = h.pages.allocToCache()
unlock(&h.lock)
}
// 尝试从P的pageCache中分配npages个页面的内存
base, scav = c.alloc(npages)
if base != 0 {
// 尝试分配一个mspan对象
s = h.tryAllocMSpan()
if s != nil && gcBlackenEnabled == 0 && (manual || spanclass.sizeclass() != 0) {
goto HaveSpan
}
}
}
lock(&h.lock)
// 走到这里base还是0,表明前面从pageCache中没有分配到内存
if base == 0 {
// 调用pageAlloc分配器进行页分配
base, scav = h.pages.alloc(npages)
if base == 0 {
// 如果还是没有分配到,进行扩容
if !h.grow(npages) {
unlock(&h.lock)
return nil
}
// 扩容之后再次调用pageAlloc分配器分配page
base, scav = h.pages.alloc(npages)
// 还是没有分配到,抛出错误
if base == 0 {
throw("grew heap, but no adequate free space found")
}
}
}
if s == nil {
// 分配一个mspan对象
s = h.allocMSpanLocked()
}
// 如果对象不是手动分配
if !manual {
// 下面是内存分配统计操作,可以不关注
memstats.heap_scan += uint64(gp.m.mcache.local_scan)
gp.m.mcache.local_scan = 0
memstats.tinyallocs += uint64(gp.m.mcache.local_tinyallocs)
gp.m.mcache.local_tinyallocs = 0
// 记录大对象分配的个数和分配的内存大小
if spanclass.sizeclass() == 0 {
mheap_.largealloc += uint64(npages * pageSize)
mheap_.nlargealloc++
atomic.Xadd64(&memstats.heap_live, int64(npages*pageSize))
}
if gcBlackenEnabled != 0 {
gcController.revise()
}
}
unlock(&h.lock)
HaveSpan:
// 将分配到的内存base和span对象s绑定起来,因为span对象管理的就是一个或多个页面
s.init(base, npages)
if h.allocNeedsZero(base, npages) {
s.needzero = 1
}
// 分配到的内存大小,一共npages*8KB个字节
nbytes := npages * pageSize
if manual {
// 大对象不是手动分配,即分配大对象不会走到这里,这里分析大对象分配流程,所以这个分支的内容不关心
...
} else {
// 根据规格(spanClass),设置span s的一些字段内容,主要有元素大小(s.elemsize)
// 元素个数(s.nelems)
s.spanclass = spanclass
// 对于大对象来说,它的spanClass对应的sizeClass为0
if sizeclass := spanclass.sizeclass(); sizeclass == 0 {
// 大对象独占一个span,所以s.nelems=1, 元素的大小就是整个span管理的页面对应的所有空间
s.elemsize = nbytes
s.nelems = 1
s.divShift = 0
s.divMul = 0
s.divShift2 = 0
s.baseMask = 0
} else {
// 这里也是处理非大对象逻辑,先不关心,具体分析放到小对象和tiny对象分配中去分析
...
}
...
// 统计内存信息,这里不用关心
mSysStatInc(sysStat, nbytes)
mSysStatDec(&memstats.heap_idle, nbytes)
...
return s
}
分配npages个页面的逻辑比较复杂,这里对其进行一个分析。每个P(GMP中的P,逻辑处理器)有一个pageCache类型的字段pcache,见下面的p结构(省略了其他字段). pcache理解为页的缓存,就是它保存了一组等待被使用的连续的页。结合pageCache的结构定义,base表示这一组连续页的起始地址,cache是一个uint64对象,在64位机器上是8个字节,即64个bit。cache中每个bit表示一个page是否是空闲的还是被分配出去了,1表示空闲,0表示已分配出去了。scav表示已清除页面的64位位图。每个页面大小为8KB,所以pcache管理的这片内存大小为64*8K=512KB。
type p struct {
... //其他省略
pcache pageCache
...
}
type pageCache struct {
base uintptr // base address of the chunk
cache uint64 // 64-bit bitmap representing free pages (1 means free)
scav uint64 // 64-bit bitmap representing scavenged pages (1 means scavenged)
}
对于要分配的的page数小于16个page的,从pcache中查找是否有空闲的空间可供分配.此时有两种情况:
对于情况2,pcache还有空闲page, 尝试从pcache中分配npages个页面。调用的函数就是alloc. alloc函数根据要分配的页数(npages)是1还是大于1走不同的处理逻辑。细节比较复杂,下面分别详细说明
分配1个页面的处理流程 就是从c.cache中找到一个bit为1的位置,该位置对应的page是未分配出去的,怎么找呢?这里有一个算法,就是从cache右边向左边找(理解为从低位向高位),对应的函数就是sys.TrailingZeros64,该函数接收1个uint64的整数,实现的功能是计算传入参数中尾随零位的总数。例如,对于15,它的二进制为1111, 二进制末尾0的个数为0个,对于12,它的二进制为1100, 二进制末尾的0的个数为2个,对于10,它的二进制为1010,二进制末尾的0的个数为1个。通俗来说,就是对于一个数x, 它对应的二进制为bx,然后从右往左统计bx中0的个数,遇到1则结束统计。
下图中cache的值为ffff ffff ffff fffa,对应的二进制为1111111111111111111111111111111111111111111111111111111111111010,对其计算sys.TrailingZeros64得到的值为1,因为它的二进制中尾随零位的总数为1,也就是第1个page是未分配的, 通过图也可以看到除了第0个和第2个page已分配外,其他都是未分配的。
然后执行c.cache &^= 1 << i,上面得到的i值为1,即c.cache &^=1<<1, 将c.cache的第1个bit为设置为0. 这里说下&^的含义,将运算符左边数据相异的位保留,相同位清零。左边c.cache的第1个bit位置为1,1<<1对应的二级制除第1个bit位为1外,其他都为0,所以执行c.cache &^ = 1<<1之后,相当于将c.cache的第1个bit位置为0,其他bit位保持不变。同理执行c.scav &^= 1 << i,将scav中第1个bit位设置为未清理状态即设置为0,c.scav中bit为1表示对应的page已清理,0表示未清理。所以如果c.scav之前第1个bit位的值为0,执行c.scav &^ = 1<<1之后,第1个bit位的值还是0,即之前是未清理状态现在还是未清理状态,如果c.scav之前的第1个bit位的值为1,执行执行c.scav &^ = 1<<1之后,第1个bit的值将变为0,即之前为已清理状态现在变为未清理状态。经过上面的操作之后,上图pageCache状态变为下图所示。
最后返回分配的内存起始地址,上面的例子中分配的是第1个page,根据pageCache的起始地址base和偏移(i的值)可计算得到第1个page的地址为c.base+1*pageSize, 返回的第二参的值为pageSize或为0,当分配前c.scav第i个bit位值为1即第i个page页已清理时,返回pageSzie,当分配前c.scav第i个bit位值为0即第i个page页未清理时,返回0.
func (c *pageCache) alloc(npages uintptr) (uintptr, uintptr) {
// c中所有的page都已分配出去了,直接返回0,0
if c.cache == 0 {
return 0, 0
}
// 如果要分配的页数是1个,走分配1个page的流程
if npages == 1 {
i := uintptr(sys.TrailingZeros64(c.cache))
scav := (c.scav >> i) & 1
// 将c.cache的第i个bit位置为0
c.cache &^= 1 << i // set bit to mark in-use
// 将c.cache
c.scav &^= 1 << i // clear bit to mark unscavenged
return c.base + i*pageSize, uintptr(scav) * pageSize
}
// 要分配的页数>1个, 走c.allocN
return c.allocN(npages)
}
分配>1个页面的处理流程 分配的页面数>1走c.allocN函数,该函数的处理逻辑也比较简单,就是从c.cache中找到一个bit位连续为1且连续的数量>=npages的位置。然后将c.cache中对应连续npages个bit位为1的值修改为0,表示这npages个bit位对应的页面已分配出去了。同理将c.scav中对应连续npages个bit位的值修改为0,表示这npages个bit位对应的页面为未清理状态。sys.OnesCount64采用Population Count算法统计一个二进制数中1的个数,感觉很巧妙,它能够在Log(N)时间复杂度内得到一个二进制数中1的个数。最后通过偏移量计算出分配内存的地址并将其返回,scav为分配前在mask对应位置为1的页面中已清理的页面数,并返回scav*pageSize的值
下图以分配3个pages为例,分配前c.cache的值如下图所示为0xffff ffff ffff fffe, 执行完findBitRange64返回1,即从c.cache的第1个索引位置的3个连续的bit值都是1,然后将其分配出去,即将它们的值都修改为0,得到新的值如分配后的图所示。
分配前
分配后
func (c *pageCache) allocN(npages uintptr) (uintptr, uintptr) {
i := findBitRange64(c.cache, uint(npages))
// i>=64 表示从c.cache中未找到连续npages个bit值为1的位置,直接返回0,0
if i >= 64 {
return 0, 0
}
// mask为找到的连续npages个bit值为1的位置的值为1,其他位置为0
// 例如如果i=3,npages=4,则mask的值为 0x0000 0000 0000 0078
mask := ((uint64(1) << npages) - 1) << i
// sys.OnesCount64采用Population Count算法统计c.scav & mask数中1的个数
scav := sys.OnesCount64(c.scav & mask)
// 将c.cache中与mask二进制中值为1的相同位置的值设为0
c.cache &^= mask // mark in-use bits
// 将c.scav中与mask二进制中值为1的相同位置的值设为0
c.scav &^= mask // clear scavenged bits
// 通过偏移量计算出分配内存的地址
// scav为分配前在mask对应位置为1的页面中已清理的页面数
return c.base + uintptr(i*pageSize), uintptr(scav) * pageSize
}
// findBitRange64从c中找到一个连续bit值为1且连续的数量>=n的位置
func findBitRange64(c uint64, n uint) uint {
i := uint(0)
cont := uint(sys.TrailingZeros64(^c))
// 当c的二进制中连续位置为1的数量(cont)大于或等于n时,表示找到一个满足条件的位置
// 如果没有找到,i的值最后会大于64,所以推出循环条件为找到了位置或i>64
for cont < n && i < 64 {
i += cont
i += uint(sys.TrailingZeros64(c >> i))
// cont记录连续1的数量
cont = uint(sys.TrailingZeros64(^(c >> i)))
}
return i
}
前面分析了分配的页面数<16个page的情况,小于16个page是从P的pageCache中分配的。下面分析分配的页面数>=16个page的情况,大于等于16个page是通过pageAlloc分配器来分配的,顺便说一下,小于16个page通过pageCache分配失败的情况下也会走这里的>=16个page的分配逻辑。下面结合源码分析>=16个page的分配流程。整个>=16个page的分配流程包含两个关键操作:
alloc利用pageAlloc分配器分配npages个页面的内存地址,分配流程比较复杂,核心是要搞懂pageAlloc分配器的工作原理。
// alloc通过pageAlloc分配器分配npages个页面的内存地址
func (s *pageAlloc) alloc(npages uintptr) (addr uintptr, scav uintptr) {
// s.end记录了已经从操作系统申请的内存最高地址的地方,pageAlloc中保存的[s.start,s.end]这段
// 内存空间是已处于sysUsed状态了(pageAlloc从操作系统申请的内存会先设置为sysReserve ,然后在设置为sysMap,最后设置为sysUsed
// 这几个状态), s.searchAddr对应的是一个堆空间地址,pageAlloc按chuck(4M)大小管理从操作系统的申请的内存
// chunkIndex(s.searchAddr)将内存地址转换成按4M切分后的索引位置,如果这个位置比s.end还大
// 说明pageAlloc暂存的page已都分配完了,这里直接返回,需要继续向操作系统申请一块内存了。
if chunkIndex(s.searchAddr) >= s.end {
return 0, 0
}
searchAddr := uintptr(0)
//pallocChunkPages为512,每个page的大小为8KB,所以一个chunk的大小为512*8KB=4M
//chunkPageIndex(s.searchAddr)得到当前的s.searchAddr的对应的第几个page页面。
//因为小于s.searchAddr地址的内存已分配出去了,pallocChunkPages-chunkPageIndex(s.searchAddr)得到
// 当前chuck剩余的页面,只有当前剩余的页面数>=npages个,才有可能分配出去npages个页面大小的空间
if pallocChunkPages-chunkPageIndex(s.searchAddr) >= uint(npages) {
// i表示s.searchAddr地址对应的是第几个chuck块
i := chunkIndex(s.searchAddr)
// s.summary[len(s(s.summary)-1][i]中的每个pallocSum维护了一个chunk块中page的分配信息
// pallocSum有3部分构成:start,max,end,start表示512个page中可以分配出去page的起始索引下标,
// end表示512个page中可以分配出去page的结束索引下标,max表示这512个page中连续为0(即未分配出去)
// 一片区域最大有多少个page
if max := s.summary[len(s.summary)-1][i].max(); max >= uint(npages) {
// 找到可以分配的起始page是第j个page
j, searchIdx := s.chunkOf(i).find(npages, chunkPageIndex(s.searchAddr))
if j < 0 {
print("runtime: max = ", max, ", npages = ", npages, "\n")
print("runtime: searchIdx = ", chunkPageIndex(s.searchAddr), ", s.searchAddr = ", hex(s.searchAddr), "\n")
throw("bad summary data")
}
// 根据chunkIndex和chunk内的偏移量j计算出实际的地址
addr = chunkBase(i) + uintptr(j)*pageSize
searchAddr = chunkBase(i) + uintptr(searchIdx)*pageSize
goto Found
}
}
// 走到这里,说明上面从s.searchAddr中未分配成功,接下来会从s.summary管理的page中
// 通过遍历找到一个有npages页面的空闲区域
addr, searchAddr = s.find(npages)
if addr == 0 {
// 还是没有分配成功
if npages == 1 {
s.searchAddr = maxSearchAddr
}
return 0, 0
}
Found:
scav = s.allocRange(addr, npages)
if s.compareSearchAddrTo(searchAddr) > 0 {
s.searchAddr = searchAddr
}
return addr, scav
}
pageAlloc分配器的数据结构和基本工作原理在组件分配部分已有介绍,这里主要是对alloc函数做一个详细分析,相信大家都能看懂。
如上图所示,pageAlloc将整个虚拟地址空间(2^48)划分成块来管理,为了方便快速找到可分配的内存,pageAlloc.summary用二维数组表示分配的摘要信息,此数组的第一层有5级,对应上图的L0到L4,可以看到L0层每个块的大小为16G,L1层每个块的大小为2G,依次下一层每个块是上一层的1/8大小,直到最后一层L4每个块大小为4M, 我们知道每个pageSize为8KB,也就是L4中每个块有512个page.因为每层块的大小是不同的,所以数组的第二维大小是不同的,L0层第二维的大小为2^48/2^34=2^14个,L4层最多,为2^48/2^22=2^26个。pageAlloc.summary数组中元素类型为pallocSum,它的定义为type pallocSum uint64, 其实就是一个uint64数字。一个pallocSum有8个字节,也就是有64个bit。将pallocSum划分成3部分:start,max,end. 如下图所示。start、max、end每一个都是位图的摘要。64/3=21,也就是每部分能分到21个bit.每个值的最大值可能为 2^21 - 1,或者所有三个值都可能等于 2^21。后一种情况仅通过设置第 64位来表示.
pallocSum中start、max和end的含义是啥呢?表示的是块内的page的分配情况。以L4层为例,每个块chunk大小为4M.也就是有512个page,编号(索引)从0到511。start表示这512个page中从第几个page是空闲的,小于start编号的page是都已分配出去了。end表示512个page中可以分配出去page的结束索引编号。max表示这512个page中连续为0(即未分配出去) 一片区域最大有多少个page。
通过pallocSum可以快速找到空闲块分配出去供应用程序使用,上图中最大连续的空闲块数即max的值为3,这时候假设要分配一个npages的值为3的空间,因为max>=npages,所以肯定可以从当前的chuck中分配到内存。假如npages的值为4,这时候max<npages,显然是从这个chuck中找不到一个块有npages页面的区域的,所以不用一个一个遍历当前chuck中的每个page(pageAlloc.chuck中的pallocData.pallocBits)查找是否满足分配要求了。
L4中每个chuck大小是4M,对应512个page,所以用9个bit就可以描述这512个page的分配还是空闲清,而pallocSum每部分是21个bit,是完全够用的。因为L0层每个块最大,那21个bit要能描述L0中块的分配情况才行。L0中每个块为16G,每个page为8KB, 所以需要2^34/2^13=2^21个数,而21bit能表示的数最大恰好为2^21-1,因此是满足的。
下面分析内存地址和这里块的关系,有两个问题?
因为对整个虚拟内存空间采用的是平坦划分,所以对于任意给定的地址addr,除以块的大小,就可以定位到这个地址属于哪个块。对于L4来说,每个块为4M, addr/4M就可以得到它在哪个块中, 然后addr%4M/8KB就可以定位到它属于块中的哪个page. 对于给定的page,采用逆向运算乘法就可以计算得到page对应的内存地址。假设空闲的page是第i个块的中的第j个页面。对于L4来说,它对于的地址计算方法为addr=i * 4M+j * 8KB=i*(512*8KB)+j * 8KB=i * (512 * pageSize)+j * pageSize.
看懂这里的分析,在结合上面的代码中的注解,理解pageAlloc.alloc方法就很容易了。
下面讲解pageAllo.alloc在分配失败时的处理,如果alloc分配失败,需要从操作系统新申请一片空间,然后继续走alloc进行分配。申请新空间处理逻辑在下面的grow方法,分为2个操作步骤:
// grow 尝试操作系统至少申请npage页的内存添加到堆中
func (h *mheap) grow(npage uintptr) bool {
//alignUp将npage向上舍入为512的倍数,也就是说npage<512,也按512个算
// 512<npages<2014时,按1024个算
ask := alignUp(npage, pallocChunkPages) * pageSize
totalGrowth := uintptr(0)
nBase := alignUp(h.curArena.base+ask, physPageSize)
// h.curArena上的内存不够,从操作系统申请更多的内存
if nBase > h.curArena.end {
// 向操作系统申请,ask为4M的整数倍
av, asize := h.sysAlloc(ask)
if av == nil {
print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)\n")
return false
}
// 新分配的内存与之前的是连续的,直接扩展h.curArena的end值
if uintptr(av) == h.curArena.end {
h.curArena.end = uintptr(av) + asize
} else {
// 新空间是不连续的。跟踪当前空间的剩余部分,并且将记录新申请的空间
if size := h.curArena.end - h.curArena.base; size != 0 {
h.pages.grow(h.curArena.base, size)
totalGrowth += size
}
// 将新申请的空间记录在h.curArena中
h.curArena.base = uintptr(av)
h.curArena.end = uintptr(av) + asize
}
// 统计分配信息,这里不关心
mSysStatInc(&memstats.heap_released, asize)
mSysStatInc(&memstats.heap_idle, asize)
// 重新计算base的值
nBase = alignUp(h.curArena.base+ask, physPageSize)
}
v := h.curArena.base
// 从nBase到h.curArea.end的空间是还未分配给pageAlloc的
h.curArena.base = nBase
// 从v到nBase的这一片空间分配给pageAlloc
h.pages.grow(v, nBase-v)
totalGrowth += nBase - v
...
return true
}
向操作系统申请内存空间调用的是sysAlloc函数,在linux系统上具体调用的是sysReserve函数,传入给此函数的是从哪里分配,分配的空间大小是多少。如果分配失败,还会尝试sysReserveAligned进行分配,该函数与sysReserve类型,不同是由内核给我们的任何充分对齐的地址。
// sysAlloc向操心系统申请内存,申请的大小为64M的整数倍
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
n = alignUp(n, heapArenaBytes)
// 32位系统会走这里的逻辑,本文分析64位系统,这里可以忽略
v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys)
if v != nil {
size = n
goto mapped
}
// 64位系统会走这里,arenaHints 是尝试添加更多堆区域的地址列表。
// 这最初由一组通用提示地址填充,并随着实际堆区域范围的边界而增长
for h.arenaHints != nil {
hint := h.arenaHints
p := hint.addr
if hint.down {
p -= n
}
if p+n < p {
v = nil
} else if arenaIndex(p+n-1) >= 1<<arenaBits {
v = nil
} else {
// 调用sysReserve向操作系统申请内存
v = sysReserve(unsafe.Pointer(p), n)
}
if p == uintptr(v) {
// 成功分配到内存
if !hint.down {
p += n
}
hint.addr = p
size = n
break
}
if v != nil {
sysFree(v, n, nil)
}
h.arenaHints = hint.next
h.arenaHintAlloc.free(unsafe.Pointer(hint))
}
if size == 0 {
if raceenabled {
throw("too many address space collisions for -race mode")
}
// 走到这里,说明上面的sysReserve(unsafe.Pointer(p), n)分配失败了,
// 注意这个分配是从地址p处分配n字节大小的空间,接下来尝试采用内核给我们的任何充分对齐的地址
// 调用sysReserveAligned分配内存没有指定从哪里开始分配,由内核给我们的任何充分对齐的地址
v, size = sysReserveAligned(nil, n, heapArenaBytes)
if v == nil {
// 还是分配失败,只能失败了
return nil, 0
}
// Create new hints for extending this region.
// 采用头插法将hint插入到mheap_.arenaHints链表中
hint := (*arenaHint)(h.arenaHintAlloc.alloc())
hint.addr, hint.down = uintptr(v), true
hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
hint = (*arenaHint)(h.arenaHintAlloc.alloc())
hint.addr = uintptr(v) + size
hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
}
...
if uintptr(v)&(heapArenaBytes-1) != 0 {
throw("misrounded allocation in sysAlloc")
}
// 将申请的内存从预留过渡到准备的状态
sysMap(v, size, &memstats.heap_sys)
mapped:
// 下面是创建管理刚分配的内存的元数据信息,h.arenas记录了分配的arena内存的对应到哪个arena块等信息
for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
l2 := h.arenas[ri.l1()]
if l2 == nil {
// 分配l2,通过persistentalloc分配器进行分配,该分配器分配的内存是永久的
l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil))
if l2 == nil {
throw("out of memory allocating heap arena map")
}
atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2))
}
if l2[ri.l2()] != nil {
throw("arena already initialized")
}
var r *heapArena
r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))
if r == nil {
// 尝试永久分配heapArena
r = (*heapArena)(persistentalloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))
if r == nil {
throw("out of memory allocating heap arena metadata")
}
}
...
}
...
return
}
至此,整个大对象(>32KB)的分配流程已经分析完,下面用一个流程图对整个过程进行一个梳理,便于我们从宏观上掌握分配的关键流程。
小对象是指大小在[16B,32KB]范围或者小于16B但是包含指针的对象。小对象的分配也是走的mallocgc函数,下面的代码摘录了小对象的分配的逻辑。概括起来小对象的分配分为三个步骤:
创建spanclass,根据分配对象的大小,结合size_to_class8或size_to_class128表,得到对象的sizeclass, 然后根据sizeclass以及对象是否包含指针计算得到spanclass
拿到1中计算得到的spanclass从mcache保存的对应规格的span,然后从span对象中获取一个空闲的object
如果2中获取失败,则会从mcentral获取一个新的span对象,加入到mcache中,然后在从刚申请的span中分配一个空的object
3.1. 在步骤3中又分为2个小步骤,先从mcentral中找到一个可用的span,mcentral维护了2个span链表mSpanList,分别是nonempty和empty,先从nonempty链表中查找是否有可用的span,如果没有在查找empty链表 3.2. 如果3.1中没有找到可用的span,则会通过mheap分配一个新的span加入到central的empty链表,并将此span给到mcache,最后从刚申请的span中分配一个空的object。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
mp := acquirem()
if mp.mallocing != 0 {
throw("malloc deadlock")
}
if mp.gsignal == getg() {
throw("malloc during signal")
}
// 设置mp在分配内存的标准位,阻止被GC回收
mp.mallocing = 1
shouldhelpgc := false
dataSize := size
c := gomcache()
var x unsafe.Pointer
// 申请的对象是否包含指针
noscan := typ == nil || typ.ptrdata == 0
// 小对象,<=32KB
if size <= maxSmallSize {
// 小于16B且不含指针对象分配
...
} else {
//含有指针 或要分配的内存在[16B,32KB]范围内,走这里的小对象分配
// 首先根据分配对象的大小size决定它的sizeclass,然后在根据sizeclass
// 确定它的spanClass
var sizeclass uint8
// size<=1024-8=1016
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
} else {
sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
}
size = uintptr(class_to_size[sizeclass])
// 根据sizeclass计算spanClass, 计算公式为 sizeclass*2+(noscan?1:0)
// noscan表示分配的对象中是否含有指针,如果有指针,spanClass为sizeclass*2
// 如果没有指针spanClass为sizeclass*2+1
spc := makeSpanClass(sizeclass, noscan)
// 从mcache中分配一个span对象
span := c.alloc[spc]
// 尝试从span对象中快速找到一个空的object
v := nextFreeFast(span)
if v == 0 {
// 分配失败,可能需要从mcentral或者mheap中获取,如果从mcentral或者mheap获取了
// 新的span,shouldhelpgc会被设置为true,表示会在下面的判断是否需要触发GC
v, span, shouldhelpgc = c.nextFree(spc)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(unsafe.Pointer(v), size)
}
}
} else {
// >32KB对象的内存分配
...
}
...
return x
}
nextFreeFast快速从mspan中找到一个空闲的object,传入的mspan来至mcache,mcache中有一个*mspan数组alloc,缓存了各个规格(span class)的span对象,每个span管理的page已按预定的size切分好了。
// nextFreeFast快速从mspan中找到下一个空闲的object,没有的话直接返回
func nextFreeFast(s *mspan) gclinkptr {
// 获取allocCache二进制中尾部0的个数,也就是获取第一个非0的bit是第几个ibt
// 在alloCache中1表示未分配,0表示已分配
theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache?
// 找到未分配的元素
if theBit < 64 {
result := s.freeindex + uintptr(theBit)
// 索引值要小于元素的数量
if result < s.nelems {
// 下一个freeidx即为当前分配的result位置的下一个
freeidx := result + 1
if freeidx%64 == 0 && freeidx != s.nelems {
return 0
}
// 对allocCache进行右移,将右边(低位)已分配的bit值(为0)移除掉
// 方便下一次快速算出allocache二级制中尾部0的个数
s.allocCache >>= uint(theBit + 1)
s.freeindex = freeidx
// 分配出去的元素数量+1
s.allocCount++
// 根据偏移量计算得到分配出去的内存地址
return gclinkptr(result*s.elemsize + s.base())
}
}
return 0
}
nextFree也是分配一个span对象,当上面的nextFreeFast分配object失败时,会调用下面的nextFree方法,该方法会尝试从mcentral中申请一个mspan,如果申请成功会加入到mcache中,并从中分配一个object.
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
// 获取一个目标规格的span
s = c.alloc[spc]
shouldhelpgc = false
freeIndex := s.nextFreeIndex()
// span中的所有元素是否已分配完,如果多已被分配,则需要获取新的span
if freeIndex == s.nelems {
// The span is full.
if uintptr(s.allocCount) != s.nelems {
println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
throw("s.allocCount != s.nelems && freeIndex == s.nelems")
}
// 申请新的span
c.refill(spc)
// 设置是否需要执行GC的标识为true,即需要检查
shouldhelpgc = true
s = c.alloc[spc]
freeIndex = s.nextFreeIndex()
}
if freeIndex >= s.nelems {
throw("freeIndex is not valid")
}
// 计算对象的起始地址:s.base()为span的首地址,freeIndex为偏移量,每个对象的大小为s.elemsize
// 所以freeIndex*s.elemsize为偏移后的地址,加上s.base()为真正的起始地址(相对于堆空间)
v = gclinkptr(freeIndex*s.elemsize + s.base())
// 分配的元素+1
s.allocCount++
if uintptr(s.allocCount) > s.nelems {
println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
throw("s.allocCount > s.nelems")
}
return
}
refill为mcache获取一个新的指定规格的span对象。此span对象中至少有一个空闲对象.
// refill为mcache获取一个新的指定规格的span对象。此span对象中至少有一个空闲对象。
func (c *mcache) refill(spc spanClass) {
s := c.alloc[spc]
// 当前span中还存在没有分配出去的对象,即span中还有空闲剩余的对象,抛出异常
if uintptr(s.allocCount) != s.nelems {
throw("refill of span with free space remaining")
}
if s != &emptymspan {
// 将此span标记为不再缓存,新分配的span的sweepgen值为mheap_.sweepgen+3
if s.sweepgen != mheap_.sweepgen+3 {
throw("bad sweepgen in refill")
}
atomic.Store(&s.sweepgen, mheap_.sweepgen)
}
// 从mcentral获取一个新的span
s = mheap_.central[spc].mcentral.cacheSpan()
if s == nil {
throw("out of memory")
}
if uintptr(s.allocCount) == s.nelems {
throw("span has no free space")
}
//指示此跨度已缓存并防止在下一个扫描阶段进行异步扫描
// mspan.sweepgen=mheap.sweepgen-2 表示还未被清扫,mspan.sweepgen=mheap.sweepgen-1
// 表示已经正处于清扫中
s.sweepgen = mheap_.sweepgen + 3
// 将新的span s 放入到mcache中
c.alloc[spc] = s
}
cacheSpan从mcentral分配一个可用的span给mcache,每个mcentral有两个mSpanList,分别为nonempty和empty。mSpanList是span对象的链表。先从nonempty分配可用的span, 会做一些清理操作。如果没有分配成功,接下来会从empty中分配。如果从nonempty和empty链表中都没有找到可用的span,则需要从mheap中进行分配。
// cacheSpan分配一个可用的span给mcache
func (c *mcentral) cacheSpan() *mspan {
...
sg := mheap_.sweepgen
retry:
var s *mspan
// 首先从nonempty链表中查找,nonempty链表表示确定该span最少有一个未分配的元素
for s = c.nonempty.first; s != nil; s = s.next {
// sweepgen每次GC都会增加2,如果s.sweepgen==mheap_.sweepgen表示span已经清扫过
// s.sweepgen==mheap_.sweepgen-1表示span正在被清理
// s.sweepgen==mheap_.sweepgen-2表示span等待被清理
// 如果span等待被清理,尝试原子修改s.sweepgen为mheap_.sweepgen-1
if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
// 修改成功,则把该span移动到empty链表,然后执行清理操作,此时拥有了一个span
c.nonempty.remove(s)
c.empty.insertBack(s)
unlock(&c.lock)
s.sweep(true)
goto havespan
}
// 如果此span正在被其他线程清理,直接跳过
if s.sweepgen == sg-1 {
continue
}
// 该span已经被清理过,并且该span在nonempty链表中,也就是此span中最少有一个未分配的元素,
// 这里可以直接使用它
c.nonempty.remove(s)
c.empty.insertBack(s)
unlock(&c.lock)
goto havespan
}
// 走到这里说明从上面的nonempty链表中没有找到span,接下来查找empty链表
for s = c.empty.first; s != nil; s = s.next {
if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
// 将span s从empty中移除
c.empty.remove(s)
// 将span s重新插入到empty链表尾部
c.empty.insertBack(s)
unlock(&c.lock)
// 执行清理操作
s.sweep(true)
freeIndex := s.nextFreeIndex()
// 对s尝试进行清理之后,如果里面有未分配的对象,则可以使用该span
if freeIndex != s.nelems {
s.freeindex = freeIndex
goto havespan
}
lock(&c.lock)
goto retry
}
// 如果此span正在被其他goroutine清理,直接跳过
if s.sweepgen == sg-1 {
// the span is being swept by background sweeper, skip
continue
}
// 走这里说明s.sweepgen == sg,即span s已经被清理过,因为清理过的span放在empty的尾部
// 所以不用继续循环了,后面的也是被清理过的,跳出循环
break
}
if trace.enabled {
traceGCSweepDone()
traceDone = true
}
unlock(&c.lock)
// 从nonempty和empty链表中都没有找到可用的span,则需要从mheap中进行分配
// 分配完之后加入到empty链表中
s = c.grow()
if s == nil {
return nil
}
lock(&c.lock)
c.empty.insertBack(s)
unlock(&c.lock)
havespan:
if trace.enabled && !traceDone {
traceGCSweepDone()
}
n := int(s.nelems) - int(s.allocCount)
if n == 0 || s.freeindex == s.nelems || uintptr(s.allocCount) == s.nelems {
throw("span has no free objects")
}
// 统计span中未分配的元素数量(n), 加入到mcentral.nmalloc中
// 统计span中未分配的元素总大小,加入到memstats.heap_live中
atomic.Xadd64(&c.nmalloc, int64(n))
usedBytes := uintptr(s.allocCount) * s.elemsize
atomic.Xadd64(&memstats.heap_live, int64(spanBytes)-int64(usedBytes))
if trace.enabled {
traceHeapAlloc()
}
if gcBlackenEnabled != 0 {
gcController.revise()
}
// 根据freeindex更新allocCache
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return s
}
grow方法从mheap中申请mspan对象,具体分配调用的是mheap_.alloc,mheap_.alloc的处理分析见前面大对象的分配中已做了详细的解析。
func (c *mcentral) grow() *mspan {
// 根据mcentral的类型(spanclass)计算需要申请的span的大小和可以保存的元素的数量
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])
// 向mheap申请一个新的span, mheap_.alloc在大对象的分配中有详细分析
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
// 分配并初始化span的allocBits和gcmarkBits
heapBitsForAddr(s.base()).initSpan(s)
return s
}
到此,小对象[16B,32KB]的分配流程已经分析完,下面用一个流程图对整个过程进行一个梳理,便于我们从宏观上掌握小对象分配的关键流程。
微对象指分配的对象小于16B,且不含指针。微对象采用tiny分配器进行分配。在分配的时候,如果分配器中保存的空间够分配,就从分配器中分配,否则从span中取一个16byte的对象,然后对其进行切分,一部分分配出去剩下的部分保存着,供下一次分配。剩下的部分保存在tiny分配器中,mcache中有个一个tiny分配器。从mcache中分配span以及从span中申请空闲对象的逻辑与分配小对象一样,在上面分配小对象流程分析中已说明,此处不再重复。下面主要分析tiny分配器的分配原理。在下面微对象分配的过程中,object对象已分配12B,还剩4B,接下来申请一个2B的内存空间,剩余的空间还够,继续从4B中分出去2B的空间,如下图2所示。
当前的分配情况如上图所示,接下来申请一个8B的内存空间,当前tiny分配器中只剩下4B空间,不够分配,则从mcache中取一个16空object,从里面切出来8B分配出去,还剩8B保存在tiny分配器中,供后续分配使用。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
mp := acquirem()
if mp.mallocing != 0 {
throw("malloc deadlock")
}
if mp.gsignal == getg() {
throw("malloc during signal")
}
// 设置mp在分配内存的标准位,阻止被GC回收
mp.mallocing = 1
shouldhelpgc := false
dataSize := size
c := gomcache()
var x unsafe.Pointer
// 申请的对象是否包含指针
noscan := typ == nil || typ.ptrdata == 0
if size <= maxSmallSize {
if noscan && size < maxTinySize {
// 分配的对象小于16B,且不含指针,称之它为微对象,采用tiny分配器进行分配
// 因为span中最小元素的大小为8byte,如果分配的对象很小,例如分配2字节,就有6个字节空间
// 被浪费掉,这里对小对象做了特殊处理,用tiny分配器进行分配。微对象大小整合在tinyClass span中
// span中每个对象的大小为16byte.在分配的时候,取一个16byte的对象,然后对其进行切分,一部分分配出去
// 切分剩下的部分保存着,供下一次分配。这个过程就是tiny分配器做的事情。
off := c.tinyoffset
// Align tiny pointer for required (conservative) alignment.
// 做点对其处理
if size&7 == 0 { // size小于16为8的倍数
off = alignUp(off, 8)
} else if size&3 == 0 { //size小于16且为4的倍数
off = alignUp(off, 4)
} else if size&1 == 0 { //size小于16且为2的倍数
off = alignUp(off, 2)
}
// 偏移off加上当前分配的大小小于等于16B,说明之前切分剩下的还够分配,就从之前剩下的
// 里面切分出size个byte
if off+size <= maxTinySize && c.tiny != 0 {
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
// 分配的小对象数+1
c.local_tinyallocs++
// 取消mp正在分配的的标志
mp.mallocing = 0
releasem(mp)
return x
}
// 从mcache中申请一个span
span := c.alloc[tinySpanClass]
// 从span中找到一个空的object,这个object的大小为16byte
v := nextFreeFast(span)
if v == 0 {
// 尝试从mcentral中分配
v, _, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
// 将申请的16byte的地址空间清理为0
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// 更新分配器中的字段,这里的意思,如果要分配的对象大小size比之前分配的要小
// 即这次分配的object切分后剩下的空间比之前的要大,则保存这次剩下的空间
// 实际效果比较这次分配剩下的空间和前次分配剩下空间,那个大保留那个。
if size < c.tinyoffset || c.tiny == 0 {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize
} else {
// 小对象分配
...
}
} else {
// 大对象分配,>32KB对象的内存分配
...
}
...
return x
}
还是照旧,对微对象分配画一个流程图,如下图所示。对源码细节不感兴趣的可以看这张流程图,从整体了解分配流程。
到这里,大对象、小对象和微对象的所有分配细节已经分析完,下面对Go对象分配整体做一个概览,得到下面的全局分配流程图,方便我们以后回顾。
Golang源码探索(三) GC的实现原理[1]详解Go语言的内存模型及堆的分配管理[2]内存分配[3]内存分配器[4]
Golang源码探索(三) GC的实现原理: https://www.cnblogs.com/zkweb/p/7880099.html
[2]详解Go语言的内存模型及堆的分配管理: https://zhuanlan.zhihu.com/p/76802887
[3]内存分配: https://golang.design/under-the-hood/zh-cn/part2runtime/ch07alloc/basic/#mheap
[4]内存分配器: https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/