当前位置:首页 > 技术前沿 > 正文

go语言学习总结(四十四)Golang 内存管理

Golang的内存管理基于tcmalloc,可以说起点挺高的。但是Golang在实现的时候还做了很多优化,我们下面通过源码来看一下Golang的内存管理实现。下面的源码分析基于。1.tcmalloc介绍关于tcmalloc可以参考这篇文章tcmalloc介绍,原始论文可以参考TCMalloc:Thr...

Golang的内存管理基于tcmalloc,可以说起点挺高的。但是Golang在实现的时候还做了很多优化,我们下面通过源码来看一下Golang的内存管理实现。下面的源码分析基于。1.tcmalloc介......

Golang的内存管理基于tcmalloc,可以说起点挺高的。但是Golang在实现的时候还做了很多优化,我们下面通过源码来看一下Golang的内存管理实现。下面的源码分析基于。

1.tcmalloc介绍

关于tcmalloc可以参考这篇文章tcmalloc介绍,原始论文可以参考TCMalloc:Thread-CachingMalloc。

2.Golang内存管理

0.准备知识

这里先简单介绍一下Golang运行调度。在Golang里面有三个基本的概念:G,M,P。

G:Goroutine执行的上下文环境。

M:操作系统线程。

P:Processer。进程调度的关键,调度器,也可以认为约等于CPU。

一个Goroutine的运行需要G+P+M三部分结合起来。好,先简单介绍到这里,更详细的放在后面的文章里面来说。

1.逃逸分析(escapeanalysis)

对于手动管理内存的语言,比如C/C++,我们使用malloc或者new申请的变量会被分配到堆上。但是Golang并不是这样,虽然Golang语言里面也有new。Golang编译器决定变量应该分配到什么地方时会进行逃逸分析。下面是一个简单的例子。

packagemainimport()funcfoo()*int{varxintreturnx}funcbar()int{x:=new(int)*x=1return*x}funcmain(){}

将上面文件保存为,执行下面命令

$gorun-gcflags'-m-l'/:6:movedtoheap:x./:7:xescapetoheap./:11:barnew(int)doesnotescape

上面的意思是foo()中的x最后在堆上分配,而bar()中的x最后分配在了栈上。在官网()FAQ上有一个关于变量分配的问题如下:

如何得知变量是分配在栈(stack)上还是堆(heap)上?

准确地说,你并不需要知道。Golang中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

知道变量的存储位置确实和效率编程有关系。如果可能,Golang编译器会将函数的局部变量分配到函数栈帧(stackframe)上。然而,如果编译器不能确保变量在函数return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数return之后,变量不再被引用,则将其分配到栈上。

2.关键数据结构

几个关键的地方:

1.mcache:per-Pcache,可以认为是localcache。

2.mcentral:全局cache,mcache不够用的时候向mcentral申请。

3.mheap:当mcentral也不够用的时候,通过mheap向操作系统申请。

可以将其看成多级内存分配器。

2.1mcache

我们知道每个Gorontine的运行都是绑定到一个P上面,mcache是每个P的cache。这么做的好处是分配内存时不需要加锁。mcache结构如下。

//Per-thread(inGo,per-P)cacheforsmallobjects.//Nolockingneededbecauseitisper-thread(per-P).typemcachestruct{//Thefollowingmembersareaccessedoneverymalloc,//_sampleint32//triggerheapsampleafterallocatingthismanybyteslocal_scanuintptr//bytesofscannableheapallocated//小对象分配器,小于16byte的小对象都会通过tiny来分配。tinyuintptrtinyoffsetuintptrlocal_tinyallocsuintptr//numberoftinyallocsnotcountedinotherstats//[_NumSizeClasses]*mspan//spanstoallocatefromstackcache[_NumStackOrders]stackfreelist//Localallocatorstats,_nlookupuintptr//numberofpointerlookupslocal_largefreeuintptr//bytesfreedforlargeobjects(maxsmallsize)local_nlargefreeuintptr//numberoffreesforlargeobjects(maxsmallsize)local_nsmallfree[_NumSizeClasses]uintptr//numberoffreesforsmallobjects(=maxsmallsize)}
//file:_to_size=[_NumSizeClasses]uint16{0,8,16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,288,320,352,384,416,448,480,512,576,640,704,768,896,1024,1152,1280,1408,1536,1792,2048,2304,2688,3072,3200,3456,4096,4864,5376,6144,6528,6784,6912,8192,9472,9728,10240,10880,12288,13568,14336,16384,18432,19072,20480,21760,24576,27264,28672,32768}

这里仔细想有个小问题,上面的alloc类似内存池的freelist数组或者链表,正常实现每个数组元素是一个链表,链表由特定大小的块串起来。但是这里统一使用了mspan结构,那么只有一种可能,就是mspan中记录了需要分配的块大小。我们来看一下mspan的结构。

2.2mspan

span在tcmalloc中作为一种管理内存的基本单位而存在。Golang的mspan的结构如下,省略了部分内容。

typemspanstruct{next*mspan//nextspaninlist,ornilifnoneprev*mspan//previousspaninlist,ornilifnonelist*mSpanList//://()npagesuintptr//numberofpagesinspanstackfreelistgclinkptr//listoffreestacks,avoidsoverloadingfreelist//freeindexistheslotindexbetween0andnelemsatwhichtobeginscanning////TODO:Lookupnelemsfromsizeclassandremovethisfieldifit////numberofobjectinthespan.//用位图来管理可用的freeobject,1表示可用allocCacheuint64sizeclassuint8//sizeclasselemsizeuintptr//computedfromsizeclassorfromnpages}

从上面的结构可以看出:

1.next,prev:指针域,因为mspan一般都是以链表形式使用。

2.npages:mspan的大小为page大小的整数倍。

3.sizeclass:0~_NumSizeClasses之间的一个值,这个解释了我们的疑问。比如,sizeclass=3,那么这个mspan被分割成32byte的块。

4.elemsize:通过sizeclass或者npages可以计算出来。比如sizeclass=3,elemsize=32byte。对于大于32Kb的内存分配,都是分配整数页,elemsize=page_size*npages。

5.nelems:span中包块的总数目。

6.freeindex:0~nelemes-1,表示分配到第几个块。

2.3mcentral

上面说到当mcache不够用的时候,会从mcentral申请。那我们下面就来介绍一下mcentral。

typemcentralstruct{lockmutexsizeclassint32nonemptymSpanList//listofspanswithafreeobject,ieanonemptyfreelistemptymSpanList//listofspanswithnofreeobjects(orcachedinanmcache)}typemSpanListstruct{first*mspanlast*mspan}

mcentral分析:

1.sizeclass:也有成员sizeclass,那么mcentral是不是也有67个呢?是的。

2.lock:因为会有多个P过来竞争。

3.nonempty:mspan的双向链表,当前mcentral中可用的mspanlist。

4.empty:已经被使用的,可以认为是一种对所有mspan的track。

问题来了,mcentral存在于什么地方?虽然在上面我们将mcentral和mheap作为两个部分来讲,但是作为全局的结构,这两部分是可以定义在一起的。实际上也是这样,mcentral包含在mheap中。

2.4mheap

Golang中的mheap结构定义如下。

typemheapstruct{lockmutexfree[_MaxMHeapList]mSpanList//freelistsofgivenlengthfreelargemSpanList//freelistslength=_MaxMHeapListbusy[_MaxMHeapList]mSpanList//busylistsoflargeobjectsofgivenlengthbusylargemSpanList//busylistsoflargeobjectslength=_MaxMHeapListsweepgenuint32//sweepgeneration,seecommentinmspansweepdoneuint32//allspansareswept////appearsexactlyonce.////Thememoryforallspansismanuallymanagedandcanbe//reallocatedandmoveastheheapgrows.////Ingeneral,allspansisprotectedbymheap_.lock,which//preventsconcurrentaccessaswellasfreeingthebacking//,but//mustensurethatallocationcannothappenaroundthe//access(sincethatmayfreethebackingstore).allspans[]*mspan//allspansoutthere//spansisalookuptabletomapvirtualaddresspageIDsto*mspan.//Forallocatedspans,theirpagesmaptothespanitself.//Forfreespans,onlythelowestandhighestpagesmaptothespanitself.//Internalpagesmaptoanarbitraryspan.//Forpagesthathaveneverbeenallocated,spansentriesarenil.////Thisisbackedbyareservedregionoftheaddressspaceso//(spans)is//(spans)[]*mspan//sweepSpanscontainstwomspanstacks:oneofsweptin-use//spans,////oneachcycle,thismeansthesweptspansarein//sweepSpans[sweepgen/2%2]andtheunsweptspansarein//sweepSpans[1-sweepgen/2%2].Sweepingpopsspansfromthe//unsweptstackandpushesspansthatarestillin-useonthe//,allocatinganin-usespanpushesit//[2]gcSweepBuf_uint32//alignuint64fieldson32-bitforatomics//ProportionalsweeppagesInUseuint64//pagesofspansinstats_MSpanInUse;R///bytesofspansallocatedthiscycle;updatedatomicallypagesSweptuint64//pagessweptthiscycle;updatedatomicallysweepPagesPerBytefloat64//proportionalsweepratio;writtenwithlock,readwithout//TODO(austin):pagesInUseshouldbeauintptr,butthe386//compilercan't8-bytealignfields.////bytesfreedforlargeobjects(maxsmallsize)nlargefreeuint64//numberoffreesforlargeobjects(maxsmallsize)nsmallfree[_NumSizeClasses]uint64//numberoffreesforsmallobjects(=maxsmallsize)//rangeofaddresseswemightseeintheheapbitmapuintptr//Pointstoonebytepasttheofthebitmapbitmap_mappeduintptrarena_startuintptrarena_useduintptr//alwaysmHeap_Map{Bits,Spans}beforeupdatingarena_uintptrarena_reservedbool//centralfreelistsforsmallsizeclasses.//thepaddingmakessurethattheMCentralsare//spacedCacheLineSizebytesapart,//[_NumSizeClasses]struct{mcentralmcentralpad[]byte}spanallocfixalloc//allocatorforspan*cacheallocfixalloc//allocatorformcache*specialfinalizerallocfixalloc//allocatorforspecialfinalizer*specialprofileallocfixalloc//allocatorforspecialprofile*speciallockmutex//lockforspecialrecordallocators.}

varmheap_mheap

mheap_是一个全局变量,会在系统初始化的时候初始化(在函数mallocinit()中)。我们先看一下mheap具体结构。

1.allspans[]*mspan:所有的spans都是通过mheap_申请,所有申请过的mspan都会记录在allspans。结构体中的lock就是用来保证并发安全的。注释中有关于STW的说明,这个之后会在Golang的GC文章中细说。

2.central[_NumSizeClasses]…:这个就是之前介绍的mcentral,每种大小的块对应一个mcentral。mcentral上面介绍过了。pad可以认为是一个字节填充,为了避免伪共享(falsesharing)问题的。FalseSharing可以参考FalseSharing-wikipedia,这里就不细说了。

3.sweepgen,sweepdone:GC相关。(Golang的GC策略是MarkSweep,这里是用来表示sweep的,这里就不再深入了。)

4.free[_MaxMHeapList]mSpanList:这是一个SpanList数组,每个SpanList里面的mspan由1~127(_MaxMHeapList-1)个page组成。比如free[3]是由包含3个page的mspan组成的链表。free表示的是freelist,也就是未分配的。对应的还有busylist。

5.freelargemSpanList:mspan组成的链表,每个元素(也就是mspan)的page个数大于127。对应的还有busylarge。

6.spans[]*mspan:记录arena区域页号(pagenumber)和mspan的映射关系。

7.arena_start,arena_,arena_used:要解释这几个变量之前要解释一下arena。arena是Golang中用于分配内存的连续虚拟地址区域。由mheap管理,堆上申请的所有内存都来自arena。那么如何标志内存可用呢?操作系统的常见做法用两种:一种是用链表将所有的可用内存都串起来;另一种是使用位图来标志内存块是否可用。结合上面一条spans,内存的布局是下面这样的。

8.spanalloc,cacheallocfixalloc:fixalloc是free-list,用来分配特定大小的块。

9.剩下的是一些统计信息和GC相关的信息,这里暂且按住不表,以后专门拿出来说。

3.初始化

在系统初始化阶段,上面介绍的几个结构会被进行初始化,我们直接看一下初始化代码:mallocinit()。

funcmallocinit(){//一些系统检测代码,略去varp,bitmapSize,spansSize,pSize,limituintptrvarreservedbool//limit=();//See(rsc):=0//系统指针大小PtrSize=8,表示这是一个64位系统。==8(limit==0||limit130){//这里的arenaSize,bitmapSize,spansSize分别对应mheap那一小节里面提到arena区大小,bitmap区大小,spans区大小。arenaSize:=round(_MaxMem,_PageSize)bitmapSize=arenaSize/(*8/2)spansSize=arenaSize/_PageSize*=round(spansSize,_PageSize)//尝试从不同地址开始申请fori:=0;i=0x7f;i++{switch{caseGOARCH=="arm64"GOOS=="darwin":p=uintptr(i)40|uintptrMask(0x001328)caseGOARCH=="arm64":p=uintptr(i)40|uintptrMask(0x004032)default:p=uintptr(i)40|uintptrMask(0x00c032)}pSize=bitmapSize+spansSize+arenaSize+_PageSize//向OS申请大小为pSize的连续的虚拟地址空间p=uintptr(sysReserve((p),pSize,reserved))ifp!=0{break}}}

//这里是32位系统代码对应的操作,略去。

p1:=round(p,_PageSize)spansStart:=p1mheap_.bitmap=p1+spansSize+==4{//Setarena_startsuchthatwecanacceptmemory//_.arena_start=0}else{mheap_.arena_start=p1+(spansSize+bitmapSize)}mheap_.arena_=p+pSizemheap_.arena_used=p1+(spansSize+bitmapSize)mheap_.arena_reserved=reservedifmheap_.arena_start(_PageSize-1)!=0{println("badpagesize",hex(p),hex(p1),hex(spansSize),hex(bitmapSize),hex(_PageSize),"start",hex(mheap_.arena_start))throw("misroundedallocationinmallocinit")}//_.init(spansStart,spansSize)_g_:=getg()_g_.=allocmcache()}

上面对代码做了简单的注释,下面详细解说其中的部分功能函数。

3.1arena相关

arena相关地址的大小初始化代码如下。

arenaSize:=round(_MaxMem,_PageSize)bitmapSize=arenaSize/(*8/2)spansSize=arenaSize/_PageSize*=round(spansSize,_PageSize)_MaxMem=uintptr(1_MHeapMap_TotalBits-1)

首先解释一下变量_MaxMem,里面还有一个变量就不再列出来了。简单来说_MaxMem就是系统为arena区分配的大小:64位系统分配512G;对于Windows64位系统,arena区分配32G。round是一个对齐操作,向上取_PageSize的倍数。实现也很有意思,代码如下。

//(n,auintptr)uintptr{return(n+a-1)^(a-1)}

bitmap用两个bit表示一个字的可用状态,那么算下来bitmap的大小为16G。读过Golang源码的同学会发现其实这段代码的注释里写的bitmap的大小为32G。其实是这段注释很久没有更新了,之前是用4个bit来表示一个字的可用状态,这真是一个悲伤的故事啊。

spans记录的arena区的块页号和对应的mspan指针的对应关系。比如arena区内存地址x,对应的页号就是page_num=(x-arena_start)/page_size,那么spans就会记录spans[page_num]=x。如果arena为512G的话,spans区的大小为512G/8K*8=512M。这里值得注意的是Golang的内存管理虚拟地址页大小为8k。

_PageSize=1_PageShift_PageShift=13

所以这一段连续的的虚拟内存布局(64位)如下:

3.2虚拟地址申请

主要是下面这段代码。

//尝试从不同地址开始申请

fori:=0;i=0x7f;i++{switch{caseGOARCH=="arm64"GOOS=="darwin":p=uintptr(i)40|uintptrMask(0x001328)caseGOARCH=="arm64":p=uintptr(i)40|uintptrMask(0x004032)default:p=uintptr(i)40|uintptrMask(0x00c032)}pSize=bitmapSize+spansSize+arenaSize+_PageSize//向OS申请大小为pSize的连续的虚拟地址空间p=uintptr(sysReserve((p),pSize,reserved))ifp!=0{break}}

初始化的时候,Golang向操作系统申请一段连续的地址空间,就是上面的spans+bitmap+arena。p就是这段连续地址空间的开始地址,不同平台的p取值不一样。像OS申请的时候视不同的OS版本,调用不同的系统调用,比如Unix系统调用mmap(mmap想操作系统内核申请新的虚拟地址区间,可指定起始地址和长度),Windows系统调用VirtualAlloc(类似mmap)。

//bsdfuncsysReserve(,nuintptr,reserved*bool){==8uint64(n)132||!=0{*reserved=falsereturnv}p:=mmap(v,n,_PROT_NONE,_MAP_ANON|_MAP_PRIVATE,-1,0)ifuintptr(p)4096{returnnil}*reserved=truereturnp}//darwinfuncsysReserve(,nuintptr,reserved*bool){*reserved=truep:=mmap(v,n,_PROT_NONE,_MAP_ANON|_MAP_PRIVATE,-1,0)ifuintptr(p)4096{returnnil}returnp}//linuxfuncsysReserve(,nuintptr,reserved*bool){p:=mmap(v,n,_PROT_NONE,_MAP_ANON|_MAP_PRIVATE,-1,0)ifuintptr(p)4096{returnnil}*reserved=truereturnp}//windowsfuncsysReserve(,nuintptr,reserved*bool){*reserved=true//visjustahint.//=(stdcall4(_VirtualAlloc,uintptr(v),n,_MEM_RESERVE,_PAGE_READWRITE))ifv!=nil{returnv}//(stdcall4(_VirtualAlloc,0,n,_MEM_RESERVE,_PAGE_READWRITE))}

3.3mheap初始化

我们上面介绍mheap结构的时候知道spans,bitmap,arena都是存在于mheap中的,从操作系统申请完地址之后就是初始化mheap了。

funcmallocinit(){p1:=round(p,_PageSize)spansStart:=p1mheap_.bitmap=p1+spansSize+==4{//Setarena_startsuchthatwecanacceptmemory//_.arena_start=0}else{mheap_.arena_start=p1+(spansSize+bitmapSize)}mheap_.arena_=p+pSizemheap_.arena_used=p1+(spansSize+bitmapSize)mheap_.arena_reserved=reservedifmheap_.arena_start(_PageSize-1)!=0{println("badpagesize",hex(p),hex(p1),hex(spansSize),hex(bitmapSize),hex(_PageSize),"start",hex(mheap_.arena_start))throw("misroundedallocationinmallocinit")}//_.init(spansStart,spansSize)//获取当前G_g_:=getg()//获取G上绑定的M的mcache_g_.=allocmcache()}

p是从连续虚拟地址的起始地址,先进行对齐,然后初始化arena,bitmap,spans地址。mheap_.init()会初始化fixalloc等相关的成员,还有mcentral的初始化。

func(h*mheap)init(spansStart,spansBytesuintptr){((mspan{}),recordspan,(h),_sys)((mcache{}),nil,nil,_sys)((specialfinalizer{}),nil,nil,_sys)((specialprofile{}),nil,nil,_sys)=false//h-mapcacheneedsnoinitfori:={[i].init()[i].init()}()()fori:={[i].(int32(i))}sp:=(*slice)(())=(spansStart)=0=int(spansBytes/)}

mheap初始化之后,对当前的线程也就是M进行初始化。

//获取当前G

_g_:=getg()

//获取G上绑定的M的mcache

_g_.=allocmcache()

3.4per-Pmcache初始化

上面好像并没有说到针对P的mcache初始化,因为这个时候还没有初始化P。我们看一下bootstrap的代码。

funcschedinit(){mallocinit()ifprocs_MaxGomaxprocs{procs=_MaxGomaxprocs}ifprocresize(procs)!=nil{}}

其中mallocinit()上面说过了。对P的初始化在函数procresize()中执行,我们下面只看内存相关的部分。

funcprocresize(nprocsint32)*p{//initializenewP'sfori:=int32(0);inprocs;i++{pp:=allp[i]ifpp==nil{pp=new(p)==_=[:0]fori:={[i]=[i][:0]}atomicstorep((allp[i]),(pp))}//Pmcache初始化==nil{ifold==0i==0{ifgetg().==nil{throw("missingmcache?")}//P[0]分配给主=getg().//bootstrap}else{//P[0]之外的P申请=allocmcache()}}}}

所有的P都存放在一个全局数组allp中,procresize()的目的就是将allp中用到的P进行初始化,同时对多余P的资源剥离。

4.内存分配

先说一下给对象object分配内存的主要流程:

1.objectsize32K,则使用mheap直接分配。

2.objectsize16byte,使用mcache的小对象分配器tiny直接分配。(其实tiny就是一个指针,暂且这么说吧。)

3.objectsize16bytesize=32Kbyte时,先使用mcache中对应的sizeclass分配。

4.如果mcache对应的sizeclass的span已经没有可用的块,则向mcentral请求。

5.如果mcentral也没有可用的块,则向mheap申请,并切分。

6.如果mheap也没有合适的span,则想操作系统申请。

我们看一下在堆上,也就是arena区分配内存的相关函数。

packagemainfuncfoo()*int{x:=1returnx}funcmain(){x:=foo()println(*x)}

根据之前介绍的逃逸分析,foo()中的x会被分配到堆上。把上面代码保存为看一下汇编代码。

$gobuild-gcflags'-l'-$gotoolobjdump-s"main\.foo"(SB)/Users/didi/code/go/malloc_example/:30x204065488b0c25a0080000GSMOVQGS:0x8a0,:30x2049483b6110CMPQ0x10(CX),:30:30x204f4883ec10SUBQ$0x10,:40x2053488d1d66460500LEAQ0x54666(IP),:40x205a48891c24MOVQBX,0(SP):40(SB):40x2063488b442408MOVQ0x8(SP),:40x206848c70001000000MOVQ$0x1,0(AX):50x206f4889442418MOVQAX,0x18(SP):50x20744883c410ADDQ$0x10,:50:30_noctxt(SB):30(SB)

堆上内存分配调用了runtime包的newobject函数。

funcnewobject(typ*_type){returnmallocgc(,typ,true)}funcmallocgc(sizeuintptr,typ*_type,needzerobool){c:=gomcache():=typ==nil||!=0ifsize=maxSmallSize{//objectsize=32KifnoscansizemaxTinySize{//小于16byte的小对象分配off:=//Aligntinypointerforrequired(conservative)==0{off=round(off,8)}elseifsize3==0{off=round(off,4)}elseifsize1==0{off=round(off,2)}ifoff+size=!=0{//=(+off)=off+_tinyallocs++=0releasem(mp)returnx}//:=[tinySizeClass]v:=nextFreeFast(span)ifv==0{v,_,shouldhelpgc=(tinySizeClass)}x=(v)(*[2]uint64)(x)[0]=0(*[2]uint64)(x)[1]=0//Seeifweneedtoreplacetheexistingtinyblockwiththenewone//||==0{=uintptr(x)=size}size=maxTinySize}else{//objectsize=16byteobjectsize=32Kbytevarsizeclassuint8ifsize=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])span:=[sizeclass]v:=nextFreeFast(span)ifv==0{v,span,shouldhelpgc=(sizeclass)}x=(v)!=0{memclrNoHeapPointers((v),size)}}}else{//objectsize32Kbytevars*mspanshouldhelpgc=truesystemstack(func(){s=largeAlloc(size,needzero)})=1=1x=(())size=}}

整个分配过程可以根据objectsize拆解成三部分:size16byte,16byte=size=32Kbyte,size32Kbyte。

4.1size小于16byte

对于小于16byte的内存块,mcache有个专门的内存区域tiny用来分配,tiny是指针,指向开始地址。

funcmallocgc(){off:=//地址对齐ifsize7==0{off=round(off,8)}elseifsize3==0{off=round(off,4)}elseifsize1==0{off=round(off,2)}//分配ifoff+size=!=0{//=(+off)=off+_tinyallocs++=0releasem(mp)returnx}//tiny不够了,为其重新分配一个16byte内存块span:=[tinySizeClass]v:=nextFreeFast(span)ifv==0{v,_,shouldhelpgc=(tinySizeClass)}x=(v)//将申请的内存块全置为0(*[2]uint64)(x)[0]=0(*[2]uint64)(x)[1]=0//Seeifweneedtoreplacetheexistingtinyblockwiththenewone//basedonamountofremainingfreespace.//如果申请的内存块用不完,则将剩下的给tiny,用tinyoffset记录分配了多少。||==0{=uintptr(x)=size}size=maxTinySize}

如上所示,tinyoffset表示tiny当前分配到什么地址了,之后的分配根据tinyoffset寻址。先根据要分配的对象大小进行地址对齐,比如size是8的倍数,tinyoffset和8对齐。然后就是进行分配。如果tiny剩余的空间不够用,则重新申请一个16byte的内存块,并分配给object。如果有结余,则记录在tiny上。

4.2size大于32Kbyte

对于大于32Kb的内存分配,直接跳过mcache和mcentral,通过mheap分配。

funcmallocgc(){}else{vars*mspanshouldhelpgc=truesystemstack(func(){s=largeAlloc(size,needzero)})=1=1x=(())size=}}funclargeAlloc(sizeuintptr,needzerobool)*mspan{npages:=size_PageShiftifsize_PageMask!=0{npages++}s:=mheap_.alloc(npages,0,true,needzero)ifs==nil{throw("outofmemory")}=()+sizeheapBitsForSpan(()).initSpan(s)returns}

对于大于32K的内存分配都是分配整数页,先右移然后低位与计算需要的页数。

4.3size介于16和32K

对于size介于16~32Kbyte的内存分配先计算应该分配的sizeclass,然后去mcache里面alloc[sizeclass]申请,如果[sizeclass]不足以申请,则mcache向mcentral申请,然后再分配。mcentral给mcache分配完之后会判断自己需不需要扩充,如果需要则想mheap申请。

funcmallocgc(){}else{varsizeclassuint8//计算sizeclassifsize=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])span:=[sizeclass]//从对应的span里面分配一个objectv:=nextFreeFast(span)ifv==0{v,span,shouldhelpgc=(sizeclass)}x=(v)!=0{memclrNoHeapPointers((v),size)}}}

我们首先看一下如何计算sizeclass的,预先定义了两个数组:size_to_class8和size_to_class128。数组size_to_class8,其第i个值表示地址区间((i-1)*8,i*8](smallSizeDiv=8)对应的sizeclass,size_to_class128类似。小于1024-8=1016(smallSizeMax=1024),使用size_to_class8,否则使用数组size_to_class128。看一下数组具体的值:0,1,2,3,3,4,4…。举个例子,比如要分配17byte的内存(16byte以下的使用分配),sizeclass=size_to_calss8[(17+7)/8]=size_to_class8[3]=3。不得不说这种用空间换时间的策略确实提高了运行效率。

计算出sizeclass,那么就可以去[sizeclass]分配了,注意这是一个mspan指针,真正的分配函数是nextFreeFast()函数。如下。

//nextFreeFastreturnsthenextfreeobjectifoneisquicklyavailable.//(s*mspan)gclinkptr{theBit:=()//IsthereafreeobjectintheallocCache?iftheBit64{result:=+uintptr(theBit){freeidx:=result+1iffreeidx%64==0freeidx!={return0}=(theBit+1)=freeidxv:=gclinkptr(result*+())++returnv}}return0}

allocCache这里是用位图表示内存是否可用,1表示可用。然后通过span里面的freeindex和elemsize来计算地址即可。

如果[sizeclass]已经不够用了,则从mcentral申请内存到mcache。

//nextFreereturnsthenextfreeobjectfromthecachedspanifoneisavailable.//Otherwiseitrefillsthecachewithaspanwithanavailableobjectand//returnsthatobjectalongwithaflagindicatingthatthiswasaheavy////determinewhetheranewGCcycleneedstobestartedoriftheGCisactive//(c*mcache)nextFree(sizeclassuint8)(vgclinkptr,s*mspan,shouldhelpgcbool){s=[sizeclass]shouldhelpgc=falsefreeIndex:=()iffreeIndex=={//()!={println("runtime:=",,"=",)throw("!===")}systemstack(func(){//这个地方mcache向mcentral申请(int32(sizeclass))})shouldhelpgc=trues=[sizeclass]//mcache向mcentral申请完之后,再次从mcache申请freeIndex=()}}//nextFreeIndexreturnstheindexofthenextfreeobjectinsat////Therearehardwareinstructionsthatcanbeusedtomakethis//fasterifprofilingwarrantsit.//这个函数和nextFreeFast有点冗余了func(s*mspan)nextFreeIndex()uintptr{}mcache向mcentral,如果mcentral不够,则向mheap申请。func(c*mcache)refill(sizeclassint32)*mspan{//向mcentral申请s=mheap_.central[sizeclass].()returns}//(c*mcentral)cacheSpan()*mspan{//=()}func(c*mcentral)grow()*mspan{npages:=uintptr(class_to_allocnpages[])size:=uintptr(class_to_size[])n:=(npages_PageShift)/size//这里想mheap申请s:=mheap_.alloc(npages,,false,true)returns}如果mheap不足,则想OS申请。接上面的代码mheap_.alloc()func(h*mheap)alloc(npageuintptr,sizeclassint32,largebool,needzerobool)*mspan{vars*mspansystemstack(func(){s=_m(npage,sizeclass,large)})}func(h*mheap)alloc_m(npageuintptr,sizeclassint32,largebool)*mspan{s:=(npage)}func(h*mheap)allocSpanLocked(npageuintptr)*mspan{s=(npage)ifs==nil{if!(npage){returnnil}s=(npage)ifs==nil{returnnil}}}func(h*mheap)grow(npageuintptr)bool{//Askforabigchunk,toreducethenumberofmappings//theoperatingsystemneedstotrack;alsoamortizes//theoverheadofanoperatingsystemmapping.//=round(npage,(6410)/_PageSize)ask:=npage_PageShiftifask_HeapAllocChunk{ask=_HeapAllocChunk}v:=(ask)}

整个函数调用链如上所示,最后sysAlloc会调用系统调用(mmap或者VirtualAlloc,和初始化那部分有点类似)去向操作系统申请。

5.内存回收

这里只会介绍一些简单的内存回收,更详细的垃圾回收之后会单独写一篇文章来讲。

5.1mcache回收

mcache回收可以分两部分:第一部分是将alloc中未用完的内存归还给对应的mcentral。

funcfreemcache(c*mcache){systemstack(func(){()lock(mheap_.lock)purgecachedstats(c)mheap_.((c))unlock(mheap_.lock)})}func(c*mcache)releaseAll(){fori:=0;i_NumSizeClasses;i++{s:=[i]ifs!=emptymspan{mheap_.central[i].(s)[i]=emptymspan}}//=0=0}

函数releaseAll()负责将中各个sizeclass中的mspan归还给mcentral。这里需要注意的是归还给mcentral的时候需要加锁,因为mcentral是全局的。除此之外将剩下的mcache(基本是个空壳)归还给,其实就是把mcache插入freelist表头。

func(f*fixalloc)free(){=:=(*mlink)(p)==v}

5.2mcentral回收

当mspan没有freeobject的时候,将mspan归还给mheap。

func(c*mcentral)freeSpan(s*mspan,preservebool,wasemptybool)bool{lock()!=0{unlock()returnfalse}(s)unlock()mheap_.freeSpan(s,0)returntrue}

5.3mheap

mheap并不会定时向操作系统归还,但是会对span做一些操作,比如合并相邻的span。

6.总结

tcmalloc是一种理论,运用到实践中还要考虑工程实现的问题。学习Golang源码的过程中,除了知道它是如何工作的之外,还可以学习到很多有趣的知识,比如使用变量填充CacheLine避免FalseSharing,利用debruijn序列求解TrailingZero(在函数中使用)等等。我想这就是读源码的意义所在吧。


最新文章