go语言中关于内存分配详解

介绍

目前Go语言支持GDB、LLDB和Delve几种调试器。只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。项目地址:

安装:

/go-delve/delve/cmd/dlv

首先编写一个的一个例子:

Copypackagemainimport"fmt"typeAstruct{teststring}funcmain(){a:=new(A)(a)}

然后命令行进入包所在目录,然后输入dlvdebug命令进入调试:

CopyPSC:\document\code\test_go\srcdlvdebugType'help'forlistofcommands.

然后可以使用break命令在main包的main方法上设置一个断点:

Copy(dlv)()c:/document/code/test_go/src/:8

通过breakpoints查看已经设置的所有断点:

Copy(dlv)()c:/software/go/src/runtime/:1162(0)()c:/software/go/src/runtime/:1189(0)_()c:/document/code/test_go/src/:8(0)

通过continue命令让程序运行到下一个断点处:

Copy(dlv)()c:/document/code/test_go/src/:8(hitsgoroutine(1):1total:1)(PC:0x4bd30a)3:import"fmt"4:5:typeAstruct{6:teststring7:}=8:funcmain(){9:a:=new(A)10:(a)11:}12:13:

通过disassemble反汇编命令查看main函数对应的汇编代码:

Copy(dlv)(SB)C:/document/code/test_go/src/:80x4bd2f065488b0c2528000000movrcx,qwordptrgs:[0x28]:80x4bd2f9488b8900000000movrcx,qwordptr[rcx]:80x4bd300483b6110cmprsp,qwordptr[rcx+0x10]:80x4bd3040f8697000000jbe0x4bd3a1=:80x4bd30a*4883ec78subrsp,0:80x4bd30e48896c2470movqwordptr[rsp+0x70],:80x4bd313488d6c2470learbp,ptr[rsp+0x70]:90x4bd318488d0581860100learax,ptr[__image_base__+874912]:90x4bd31f48890424movqwordptr[rsp],:90x4bd323e8e800f5ffcall$:90x4bd328488b442408movrax,qwordptr[rsp+0x8]:90x4bd32d4889442430movqwordptr[rsp+0x30],:100x4bd3324889442440movqwordptr[rsp+0x40],:100x4bd3370f57c0xorpsxmm0,:100x4bd33a0f11442448movupsxmmwordptr[rsp+0x48],:100x4bd33f488d442448learax,ptr[rsp+0x48]:100x4bd3444889442438movqwordptr[rsp+0x38],:100x4bd3498400testbyteptr[rax],:100x4bd34b488b4c2440movrcx,qwordptr[rsp+0x40]:100x4bd350488d15099f0000leardx,ptr[__image_base__+815712]:100x4bd3574889542448movqwordptr[rsp+0x48],:100x4bd35c48894c2450movqwordptr[rsp+0x50],:100x4bd3618400testbyteptr[rax],:100:100x4bd3654889442458movqwordptr[rsp+0x58],:100x4bd36a48c744246001000000movqwordptr[rsp+0x60],0:100x4bd37348c744246801000000movqwordptr[rsp+0x68],0:100x4bd37c48890424movqwordptr[rsp],:100x4bd38048c744240801000000movqwordptr[rsp+0x8],0:100x4bd38948c744241001000000movqwordptr[rsp+0x10],0:100x4bd392e869a0ffffcall$:110x4bd397488b6c2470movrbp,qwordptr[rsp+0x70]:110x4bd39c4883c478addrsp,0:110:80x4bd3a1e82a50faffcall$_noctxt.:00x4bd3a6e945ffffffjmp$

现在我们可以使用break断点到函数的调用上:

Copy(dlv)()c:/software/go/src/runtime/:1164

输入continue跳到断点的位置:

Copy(dlv)()c:/software/go/src/runtime/:1164(hitsgoroutine(1):1total:1)(PC:0x40d426)Warning:debuggingoptimizedfunction1159:}1160:1161://implementationofnewbuiltin1162://compiler(bothfrontandSSAback)knowsthesignature1163://ofthisfunction=1164:funcnewobject(typ*_type){1165:returnmallocgc(,typ,true)1166:}1167:1168://go:linknamereflect_unsafe__New1169:funcreflect_unsafe_New(typ*_type){

print命令来查看typ的数据:

Copy(dlv)printtyp*runtime._type{size:16,ptrdata:8,hash:875453117,tflag:tflagUncommon|tflagExtraStar|tflagNamed(7),align:8,fieldAlign:8,kind:25,equal:,gcdata:*1,str:5418,ptrToThis:37472}

可以看到这里打印的size是16bytes,因为我们A结构体里面就一个string类型的field。

进入到mallocgc方法后,通过args和locals命令查看函数的参数和局部变量:

Copy(dlv)argssize=(unreadablecouldnotfindloclistentryat0x8b40foraddress0x40ca73)typ=(*runtime._type)(0x4d59a0)needzero=true~r3=(unreadableemptyOPstack)(dlv)locals(nolocals)
各个对象入口
CopyfuncconvT64(valuint64)(){ifvaluint64(len(staticuint64s)){x=(staticuint64s[val])}else{x=mallocgc(8,uint64Type,false)*(*uint64)(x)=val}return}

这段代码表示如果一个int64类型的值小于256,直接十三姨的是缓存值,那么这个值不会进行内存分配。

string对象

大家在调试的时候也可以使用下面的例子来进行调试,因为go里面的对象分配是分为大对象、小对象、微对象的,所以下面准备了三个方法分别对应三种对象的创建时的调试。

Copytypesmallobjstruct{arr[110]byte}typelargeobjstruct{arr[126]byte}functiny(){y:=100000(y)}funclarge(){large:=largeobj{}println(large)}funcsmall(){small:=smallobj{}print(small)}funcmain(){//tiny()//small()//large()}
分析

内存分配是由内存分配器完成,分配器由3种组件构成:、、、。

Copytypemspanstruct{//上一个节点next*mspan//下一个节点prev*mspan//span集合list*mSpanList//span开始的地址值startAddruintptr//span管理的页数npagesuintptr//Objectnstartsataddressn*elemsize+(startpageShift).//空闲节点的索引freeindexuintptr//span中存放的对象数量nelemsuintptr//用于快速查找内存中未被使用的内存allocCacheuint64//用于计算mspan管理了多少内存elemsizeuintptr//span的结束地址值limituintptr}

是内存管理器里面的最小粒度单元,所有的对象都是被管理在mspan下面。

mspan是一个链表,有上下指针;

npages代表mspan管理的堆页的数量;

freeindex是空闲对象的索引;

nelems代表这个mspan中可以存放多少对象,等于(npages*pageSize)/elemsize;

allocCache用于快速的查找未被使用的内存地址;

elemsize表示一个对象会占用多个个bytes,等于class_to_size[sizeclass],需要注意的是sizeclass每次获取的时候会sizeclass方法,将sizeclass1;

limit表示span结束的地址值,等于startAddr+npages*pageSize;

实例图如下:


图中alloc是一个拥有137个元素的mspan数组,mspan数组管理数个page大小的内存,每个page是8k,page的数量由spanclass规格决定。

Copytypemcachestruct{//申请小对象的起始地址tinyuintptr//从起始地址tiny开始的偏移量tinyoffsetuintptr//tiny对象分配的数量local_tinyallocsuintptr//numberoftinyallocsnotcountedinotherstats//mspan对象集合,numSpanClasses=134alloc[numSpanClasses]*mspan//spanstoallocatefrom,indexedbyspanClass}

是绑在并发模型GPM的P上,在分配微对象和小对象的时候会先去中获取,每一个处理器都会被分配一个线程缓存,因此从进行分配时无需加锁。

在中有一个alloc数组,是的集合,是Go语言内存管理的基本单元。对于[16B,32KB]的对象会使用这部分span进行内存分配,所以所有在这区间大小的对象都会从alloc这个数组里寻找,下面会分析到。

Copytypemcentralstruct{//spanClassIdspanclassspanClass//空闲的span列表partial[2]spanSet//listofspanswithafreeobject//已经被使用的span列表full[2]spanSet//listofspanswithnofreeobjects//分配mspan的累积计数nmallocuint64}

当中空间不足的时候,会去中申请对应规格的mspan。获取mspan的时候会从partial列表和full列表中获取,获取的时候会使用无锁的方式获取。

在中,有spanclass标识,spanclass表示这个mcentral的类型,下面我们会看到,在分配[16B,32KB]大小对象的时候,会将对象的大小分成67组:

Copyvarclass_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}

所以只负责一种spanclass规格类型。

的数据会由两个spanSet托管,partial负责空闲的列表,full负责已被使用的列表。

CopytypeheadTailIndexuint64typespanSetstruct{//lockspineLockmutex//数据块的指针//*[N]*spanSetBlock,accessedatomically//lenspineLenuintptr//Spinearraylength,accessedatomically//capspineCapuintptr//Spinearraycap,accessedunderlock//头尾的指针,前32位是头指针,后32位是尾指针indexheadTailIndex}

spanSet这个数据结构里面有一个由index组成的头尾指针,pop数据的时候会从头获取,push数据的时候从tail放入,spine相当于数据块的指针,通过head和tail的位置可以算出每个数据块的具体位置,数据块由spanSetBlock表示:

CopyconstspanSetBlockEntries=512typespanSetBlockstruct{spans[spanSetBlockEntries]*mspan}

spanSetBlock是一个存放mspan的数据块,里面会包含一个存放512个mspan的数据指针。所以mcentral的总体数据结构如下:


Copytypemheapstruct{lockmutexpagespageAlloc//pageallocationdatastructure//arenas数组集合,一个二维数组arenas[1arenaL1Bits]*[1arenaL2Bits]*heapArena//各个规格的mcentral集合central[numSpanClasses]struct{mcentralmcentralpad[(mcentral{})%]byte}}
Copyconst(pageSize=8192//8KBheapArenaBytes=67108864//64MBpagesPerArena=heapArenaBytes/pageSize//8192)typeheapArenastruct{bitmap[heapArenaBitmapBytes]bytespans[pagesPerArena]*mspanpageInUse[pagesPerArena/8]uint8pageMarks[pagesPerArena/8]uint8zeroedBaseuintptr}

需要注意的是,上面的heapArenaBytes代表的64M只是在除windows以外的64位机器才会显示,在windows机器上显示的是4MB。具体的可以看下面的官方注释:

Copy//PlatformAddrbitsArenasizeL1entriesL2entries//------------------------------------------------------//*/64-bit4864MB14M(32MB)//windows/64-bit484MB641M(8MB)//*/32-bit324MB11024(4KB)//*/mips(le)314MB1512(2KB)

L1entries、L2entries分别代表的是中arenas一维、二维的值。


给对象分配内存
Copyfuncmallocgc(sizeuintptr,typ*_type,needzerobool){vars*mspanshouldhelpgc=truesystemstack(func(){s=largeAlloc(size,needzero,noscan)})=1=1x=(())size=}

从上面我们可以看到分配大于32KB的空间时,直接使用largeAlloc来分配一个mspan。

CopyfunclargeAlloc(sizeuintptr,needzerobool,noscanbool)*mspan{//_PageSize=8k,也就是表明对象太大,溢出ifsize+_PageSizesize{throw("outofmemory")}//_PageShift==13,计算需要分配的页数npages:=size_PageShift//如果不是整数,多出来一些,需要加1ifsize_PageMask!=0{npages++}//从堆上分配s:=mheap_.alloc(npages,makeSpanClass(0,noscan),needzero)ifs==nil{throw("outofmemory")}returns}

在分配内存的时候是按页来进行分配的,每个页的大小是_PageSize(8K),然后需要根据传入的size来判断需要分多少页,最后调用alloc从堆上分配。

Copyfunc(h*mheap)alloc(npagesuintptr,spanclassspanClass,needzerobool)*mspan{vars*mspansystemstack(func(){==0{//回收一部分内存(npages)}//进行内存分配s=(npages,false,spanclass,_inuse)})returns}

继续看allocSpan的实现:

CopyconstpageCachePages=8*(pageCache{}.cache)func(h*mheap)allocSpan(npagesuintptr,manualbool,spanclassspanClass,sysStat*uint64)(s*mspan){//:=getg()base,scav:=uintptr(0),uintptr(0)pp:=()//申请的内存比较小,尝试从pcache申请内存ifpp!=nilnpagespageCachePages/4{c:=(){lock()*c=()unlock()}base,scav=(npages)ifbase!=0{s=()ifs!=nilgcBlackenEnabled==0(manual||()!=0){gotoHaveSpan}}}lock()//内存比较大或者线程的页缓存中内存不足,从mheap的pages上获取内存ifbase==0{base,scav=(npages)//内存也不够,那么进行扩容ifbase==0{if!(npages){unlock()returnnil}//重新申请内存base,scav=(npages)//内存不足,抛出异常ifbase==0{throw("grewheap,butnoadequatefreespacefound")}}}ifs==nil{//分配一个mspan对象s=()}unlock()HaveSpan://设置参数初始化(base,npages)//建立mheap与mspan之间的联系((),npages,s)returns}

这里会根据需要分配的内存大小再判断一次:

如果要分配的页数小于pageCachePages/4=64/4=16页,那么就尝试从pcache申请内存;

如果申请的内存比较大或者线程的页缓存中内存不足,会通过从页堆分配内存;

如果页堆上内存不足,那么就mheap的grow方法从系统上申请内存,然后再调用pageAlloc的alloc分配内存;

下面来看看grow的向操作系统申请内存:

Copyfunc(h*mheap)grow(npageuintptr)bool{//:=alignUp(npage,pallocChunkPages)*pageSizetotalGrowth:=uintptr(0)nBase:=alignUp(+ask,physPageSize)//内存不够则调用sysAlloc申请内存{av,asize:=(ask)ifav==nil{print("runtime:outofmemory:cannotallocate",ask,"-byteblock(",_sys,"inuse)\n")returnfalse}//重新设置curArena的值ifuintptr(av)=={=uintptr(av)+asize}else{ifsize:=;size!=0{(,size)totalGrowth+=size}=uintptr(av)=uintptr(av)+asize}nBase=alignUp(+ask,physPageSize)}returntrue}

grow会通过curArena的值来判断是不是需要从系统申请内存;如果小于nBase那么会调用方法从操作系统中申请更多的内存;

Copyfunc(h*mheap)sysAlloc(nuintptr)(,sizeuintptr){n=alignUp(n,heapArenaBytes)//在预先保留的内存中申请一块可以使用的空间v=(n,heapArenaBytes,_sys)ifv!=nil{size=ngotomapped}//根据页堆的arenaHints在目标地址上尝试扩容!=nil{hint:=:={p-=n}ifp+np{//Wecan'tusethis,sodon'=nil}elseifarenaIndex(p+n-1)=1arenaBits{//'=nil}else{//从操作系统中申请内存v=sysReserve((p),n)}ifp==uintptr(v){//!{p+=n}=psize=nbreak}ifv!=nil{sysFree(v,n,nil)}=((hint))}//将内存由Reserved转为PreparedsysMap(v,size,_sys)mapped://Createarenametadata.//初始化一个heapArena来管理刚刚申请的内存forri:=arenaIndex(uintptr(v));ri=arenaIndex(uintptr(v)+size-1);ri++{l2:=[()]ifl2==nil{l2=(*[1arenaL2Bits]*heapArena)(persistentalloc((*l2),,nil))ifl2==nil{throw("outofmemoryallocatingheaparenamap")}(([()]),(l2))}varr*heapArenar=(*heapArena)(((*r),,_sys))//将创建heapArena放入到arenas列表中=[:len()+1][len()-1]=((l2[()]),(r))}return}

sysAlloc方法会调用预先保留的内存中申请一块可以使用的空间;如果没有会调用sysReserve方法会从操作系统中申请内存;最后初始化一个heapArena来管理刚刚申请的内存,然后将创建heapArena放入到arenas列表中。

至此,大对象的分配流程至此结束。

小对象分配
Copyfuncmallocgc(sizeuintptr,typ*_type,needzerobool){dataSize:=size//获取mcache,用于处理微对象和小对象的分配c:=gomcache()//表示对象是否包含指针,true表示对象里没有指针noscan:=typ==nil||==0//maxSmallSize=3276832kifsize=maxSmallSize{//maxTinySize=16bytesifnoscansizemaxTinySize{off:=//指针内存对齐ifsize7==0{off=alignUp(off,8)}elseifsize3==0{off=alignUp(off,4)}elseifsize1==0{off=alignUp(off,2)}//判断指针大小相加是否超过16ifoff+size=!=0{//获取tiny空闲内存的起始位置x=(+off)//重设偏移量=off+size//统计数量_tinyallocs++=0releasem(mp)returnx}//重新分配一个内存块span:=[tinySpanClass]v:=nextFreeFast(span)ifv==0{v,_,shouldhelpgc=(tinySpanClass)}x=(v)//将申请的内存块全置为0(*[2]uint64)(x)[0]=0(*[2]uint64)(x)[1]=0//如果申请的内存块用不完,则将剩下的给tiny,用tinyoffset记录分配了多少。||==0{=uintptr(x)=size}size=maxTinySize}}returnx}

在分配对象内存的时候做了一个判断,如果该对象的大小小于16bytes,并且是不包含指针的,那么就可以看作是微对象。

在分配微对象的时候,会先判断一下tiny指向的内存块够不够用,如果tiny剩余的空间超过了size大小,那么就直接在tiny上分配内存返回;


这里我再次使用我上面的图来加以解释。首先会去mcache数组里面找到对应的span,tinySpanClass对应的span的属性如下:

CopystartAddr:824635752448,npages:1,manualFreeList:0,freeindex:128,nelems:512,elemsize:16,limit:824635760640,allocCount:128,spanclass:tinySpanClass(5),

tinySpanClass对应的mspan里面只有一个page,里面的元素可以装512(nelems)个;page里面每个对象的大小是16bytes(elemsize),目前已分配了128个对象(allocCount),当然我上面的page画不了这么多,象征性的画了一下。

上面的图中还画了在page里面其中的一个object已经被使用了12bytes,还剩下4bytes没有被使用,所以会更新tinyoffset与tiny的值。

总结#

本文先是介绍了如何对go的汇编进行调试,然后分了三个层次来讲解go中的内存分配是如何进行的。对于小于32k的对象来说,go通过无锁的方式可以直接从mcache获取到了对应的内存,如果mcache内存不够的话,先是会到mcentral中获取内存,最后才到mheap中申请内存。对于大对象(32k)来说可以直接mheap中申请,但是对于大对象来说也是有一定优化,当大对象需要分配的页小于16页的时候会直接从pageCache中分配,否则才会从堆页中获取。

免责声明:本文章如果文章侵权,请联系我们处理,本站仅提供信息存储空间服务如因作品内容、版权和其他问题请于本站联系