从源码讲解 golang 内存分配

polaris · · 445 次点击 · · 开始浏览    
## 引言 golang 是谷歌2009年发布的开源编程语言,截止目前go的release版本已经到了1.12,Golang 语言专门针对多处理器系统应用程序的编程进行了优化,使用 Golang 编译的程序可以媲美 C /C++代码的速度,而且更加安全、支持并行进程。和其他“高级语言”一样,golang同样有一套自己的内存管理机制,自主的去完成内存分配、垃圾回收、内存管理等过程,从而避免频繁的向操作系统申请、释放内存,有效的提升go语言的处理性能。由于篇幅有限,本文重点针对golang1.12.6版本就内存分配情况进行一下梳理和讲解。golang的内存管理是基于tcmalloc模型设计,但又有些差异,局部缓存并不是分配给进程或者线程,而是分配给P(Processor);golang的GC是stop the world,并不是每个进程单独进行GC;golang语言对span的管理更有效率。 在进入正题之前,我们先回顾一下c语言内存是如何申请的,常用方式是调用malloc函数,指定要分配的大小,直接向操作系统申请,那我们来思考一下这种方式有没有什么问题?它会涉及到用户态和内核态的切换过程,那么频繁的进行用户态和内核态切换就会带来很大的耗时,导致性能下降,因此我们必须从语言层面找一种方式减少这么操作,那就是自己做一套内存管理机制。 就内存管理来说,如果要让我们去设计,思考一下都需要哪些功能模块才能保证高效稳定? - 内存池:要减少用户态和内核态的频繁切换就需要自己申请一块内存空间,将之分割成大小规格不同的内存块来供程序使用,内存池是再适合不过的组成部分了。 - GC:内存管理不光需要使用方便,还要保证内存使用过程能够节约,毕竟整个系统的内存资源是有限的,那么就需要GC进行动态的垃圾回收,销毁无用的对象,释放内存来保证整个程序乃至系统运行平稳。 - 锁:一个应用程序内部之间存在大量的线程,线程之间资源是共享的,那么要保证同一块内存使用过程不出现复用或者污染,就必须保证同一时间只能有一个线程进行申请,第一个想到的肯定是锁,对公共区域的资源一定要加锁,另一种方式就是内存隔离,这个在golang的mcache中会有体现。 下面我们进入正题,基于上面分析的问题对golang进行一下研究,看看golang到底怎么管理内存的。 ## 基本概念 ### 1.什么是span 首先我们来介绍一下span的概念,span是golang内存管理的基本单位,每个span管理指定规格(以page为单位)的内存块,内存池分配出不同规格的内存块就是通过span体现出来的,应用程序创建对象就是通过找到对应规格的span来存储的,下面我们看一下mspan的结构 go1.12.6\src\runtime\mheap.go ```go //go:notinheap type mspan struct { next *mspan //链表下一个span地址 prev *mspan // 链表前一个span地址 list *mSpanList // 链表地址 startAddr uintptr // 该span在arena区域的起始地址 npages uintptr // 该span占用arena区域page的数量 manualFreeList gclinkptr // 空闲对象列表 freeindex uintptr//freeindex是0到nelems之间的位置索引,标记下一个空对象索引 nelems uintptr // 管理的对象数 allocCache uint64 //从freeindex开始的位标记 allocBits *gcBits //该mspan中对象的位图 gcmarkBits *gcBits //该mspan中标记的位图,用于垃圾回收 sweepgen uint32 //扫描计数值,用户与mheap的sweepgen比较,根据差值确定该span的扫描状态 allocCount uint16 // 已分配的对象的个数 spanclass spanClass // span分类 state mSpanState // mspaninuse etc needzero uint8 // 分配之前需要置零 scavenged bool // 标记是否内存已经被系统回收,大对象会用到 elemsize uintptr // 对象的大小 unusedsince int64 // 空闲状态开始的纳秒值时间戳,用于系统内存释放 limit uintptr // 申请大对象内存块会用到,mspan的数据截止位置 ``` ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/16646BE45D304FFA8A6C3387B0174323/2725) 根据源码和上图结合来看,会更加容易理解mspan,每一个mspan就是用来给程序分配对象空间的,也就是说一般我们对象都会放到mspan中管理,这里我们重点解释一下如图所示的几个属性,startAddr 是该mspan在arena区域的首地址,freeindex 用来表示下一个可能是空对象的位置,也就是说freeindex之前的元素(存储对象的空间)均是已经被使用的,freeindex之后的元素可能被使用可能没被使用,allocCache是从freeindex开始对后续元素分配情况进行缓存标记,通过freeindex和allocCache结合进行查找未分配的元素位置效率会更高,我们能快速的找到一个空对象分配给程序使用,而不用全局遍历。allocBits用来标识该span中所有元素的使用分配情况,gcmarkBits 用来sweep过程进行标记垃圾对象的,用于后续gc。 ### 2.怎么区分span 那么要想区分不同规格的span,我们必须要有一个标识,每个span通过splanclass标识属于哪种规格的span,golang的span规格一共有67种,具体查看 go1.12.6\src\runtime\sizeclasses.go,可看到下图的规格表 ```go // class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 3 32 8192 256 0 46.88% // 4 48 8192 170 32 31.52% // 5 64 8192 128 0 23.44% // 6 80 8192 102 32 19.07% // 7 96 8192 85 32 15.95% // 8 112 8192 73 16 13.56% // 9 128 8192 64 0 11.72% ... //省略部分规格,详细请看源文件 ... // 59 18432 73728 4 0 11.11% // 60 19072 57344 3 128 3.57% // 61 20480 40960 2 0 6.87% // 62 21760 65536 3 256 6.25% // 63 24576 24576 1 0 11.45% // 64 27264 81920 3 128 10.00% // 65 28672 57344 2 0 4.91% // 66 32768 32768 1 0 12.50% ``` 其中: - class: 分类id或者规格id,也就是spanclass, 表示该span可存储的对象规格类型 - bytes/obj:该列代表能存储每个对象的字节数,也就是说可以存储多大的对象,字段是elemsize - bytes/span:每个span占用堆的字节数,也即页数*页大小,npages*8KB - objects: 每个span可分配的元素个数,或者说可存储的对象个数,也就是nelems,也即(bytes/spans)/(bytes/obj) - tail bytes: 每个span产生的内存碎片,也即(bytes/span)%(bytes/obj) - max waste:最大浪费比例,(bytes/obj-最小使用量)*objects/(bytes/span)*100,比如classId=2 最小使用量是9bytes,则max waste=(16-9)*512/8192*100=43.75% 通过上表,我们可以很清楚的知道在创建一个对象时候,需要去选哪一个splanclass的span去获取内存空间,一个span能存多少这样大小的对象等等信息,非常清晰而又尽可能节约的去使用内存。另外上表可见最大的对象是32KB大小,超过32KB大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。所以上面只有列出了1-66。 ### 内存管理组件 阐述完一些基本概念,我们可以知道对象是存在span中,大家肯定会疑惑那span放在哪,怎么把这些各种规格孤立的span串起来?下面我们来说一下golang的内存管理组件,内存分配是由内存分配器完成,分配器由3种组件构成:mcache、mcentral、mheap,我们来详细讲一下每个组件。 我们知道golang之所有有很强的并发能力,依赖于它的G-P-M并发模型, G-P-M 并发模型: - G:表示Goroutine,也就是我们说的协程,由P进行调度。 - P:是 G-M 的中间层,是逻辑处理器,每一个P组织多个 goroutine 跑在同一个 OS 线程上。 - M:是对内核级线程的封装,真正干活的对象。 P作为协调器将G队列动态的绑在不同的M上,根据负载情况动态调整,从而均衡的发挥出多核最大并行处理计算能力 ### 1.mcache mcache就绑在并发模型的P上,也就是说我们每一个P都会有一个mcahe绑定,用来给协程分配对象存储空间的。下面具体看一下mcache的结构 go1.12.6\src\runtime\mcache.go ```go //go:notinheap type mcache struct { tiny uintptr //<16byte 申请小对象的起始地址 tinyoffset uintptr //从起始地址tiny开始的偏移量 local_tinyallocs uintptr //tiny对象分配的数量 alloc [numSpanClasses]*mspan // 分配的mspan list,其中numSpanClasses=134,索引是splanclassId stackcache [_NumStackOrders]stackfreelist //栈缓存 local_largefree uintptr // 大对象释放字节数 local_nlargefree uintptr // 释放的大对象数量 local_nsmallfree [_NumSizeClasses]uintptr // 每种规格小对象释放的个数 flushGen uint32 //扫描计数 } ``` 可以看到在mcache结构体中并没有锁存在,这是因为每个P都会绑定一个mcache,而每个P同时只会处理一个groutine,而且不同P之间是内存隔离的,因此不存在竞争情况。关键字段都已经在代码中解释了,这里我们重点关注一下 `alloc [numSpanClasses]*mspan`,由于SpanClasses一共有67种,为了满足指针对象和非指针对象,这里为每种规格的span同时准备scan和noscan两个,因此一共有134个mspan缓存链表,分别用于存储指针对象和非指针对象,这样对非指针对象扫描的时候不需要继续扫描它是否引用其他对象,GC扫描对象的时候对于noscan的span可以不去查看bitmap区域来标记子对象, 这样可以大幅提升标记的效率。另外mcache在初始化时是没有任何mspan资源的,在使用过程中会动态地申请,不断的去填充 alloc[numSpanClasses]*mspan,通过双向链表连接,如下图所示: ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/088B80BB19144308A81AA4DADF92F3F5/2736) 通过图示我们可以看到alloc[numSpanClasses]*mspan管理了很多不同规格不同类型的span,golang对于[16B,32KB]的对象会使用这部分span进行内存分配,所以所有在这区间大小的对象都会从alloc这个数组里寻找,看下源码: ```go var sizeclass uint8 //确定规格 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]) spc := makeSpanClass(sizeclass, noscan) //alloc中查到 span := c.alloc[spc] ``` 而对于更小的对象,我们叫它tiny对象,golang会通过tiny和tinyoffset组合寻找位置分配内存空间,这样可以更好的节约空间。源码如下: ```go off := c.tinyoffset //根据不同大小内存对齐 if size&7 == 0 { off = round(off, 8) } else if size&3 == 0 { off = round(off, 4) } else if size&1 == 0 { off = round(off, 2) } if off+size <= maxTinySize && c.tiny != 0 { // tiny+偏移量 x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.local_tinyallocs++ mp.mallocing = 0 releasem(mp) return x } // 空间不足从alloc重新申请空间用于tiny对象分配 span := c.alloc[tinySpanClass] ``` ### 2.mcentral 刚才我们提到mcache中的mspan都是动态申请的,那到底是去哪里申请呢?其实当空间不足的时候,mcache会去mcentral中申请对应规格的mspan,我们来继续看一下mcentral,先来看一下结构,go1.12.6\src\runtime\mcentral.go ```go //go:notinheap type mcentral struct { lock mutex spanclass spanClass //spanClass Id nonempty mSpanList // 空闲的span列表 empty mSpanList // 已经被使用的span列表 nmalloc uint64 //这个mcentral分配mspan的累积计数 } ``` 看到mcentral的结构体会觉得很简单,首先与mcache有一个明显区别,就是有锁存在,由于mcentral是公共资源,会有多个mcache向它申请mspan,因此必须加锁,另外,mcentral与mcache不同,由于P绑定了很多Goroutine,在P上会处理不同大小的对象,mcache就需要包含各种规格的mspan,但mcentral不同,同一个mcentral只负责一种规格的mspan就够了,mcache就像一个市政府,mcentral就像国家部委,市政府需要管管辖区域内的所有方面的事情,而每个部委很专一,只管一方面,市政府需要哪方面资源,就去和对应部委对接就可以了。mcentral也是用spanclass 进行标记规格类型,该规格的所有未被使用的空闲mspan会挂载到nonempty 链表上,已经被mcache拿走,未归还的会挂载到empty 链表上,归还后会再挂载到nonempty上,用图表示如下,以规格sizeClass=1为例: ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/ACE7FE7AD83F453AB5E68CDB39F6250A/2738) 每一个mSpanList都挂着同一规格mspan双向链表,当然这个链表也不是固定大小的,都会动态变化的。 ### 3.mheap mcentral 的nonempty也有用完的时候,当nonempty为空,再被申请的时候,也就是mcentral空间不足了,那么它会向mheap申请新的页,下面我们看一下mheap结构。 go1.12.6\src\runtime\mheap.go ```go //go:notinheap type mheap struct { lock mutex free mTreap // 空闲的并且没被os收回的二叉树堆,大对象用 scav mTreap // 空闲的并且已经被os收回的二叉树堆,大对象用 sweepgen uint32 // 扫描计数值,每次gc后会自增2 sweepdone uint32 // 扫描状态,用于判断是否可以进行一次扫描 sweepers uint32 // number of active sweepone calls allspans []*mspan // 所有的spans //sweepSpans的长度是2,sweepSpans[h.sweepgen/2%2]保存当前正在使用的span列表 //sweepSpans[1-h.sweepgen/2%2]保存等待sweep的span列表,由于sweepgen每次gc+2,因此 //sweepSpans [0],sweepSpans [1]每次身份互相交换 sweepSpans [2]gcSweepBuf _ uint32 // align uint64 fields on 32-bit for atomics pagesInUse uint64 // 有多少页正在被使用 pagesSwept uint64 // 扫描的页面数量 pagesSweptBasis uint64 // 用做扫描比例的初始基点 sweepHeapLiveBasis uint64 // 用做扫描比例的初始处于存活状态的初始基点 sweepPagesPerByte float64 //扫描比 //是一个全局page页要被回收的位置,通过它/单个heapArena的总页数可以判断出是第几个heapArena //通过它%单个heapArena总page数,可以确定在这个heapArena的位置 reclaimIndex uint64 reclaimCredit uintptr //多归还的pages,是回收对象在heapArena释放的 scavengeCredit uintptr //多回收给os的字节,下次回收可先扣减这个值,不足再回收真正的空间 // Malloc stats. largealloc uint64 // 大对象分配的字节数 nlargealloc uint64 // 大对象分配的数量 largefree uint64 // 大对象释放的字节数 nlargefree uint64 // 大对象释放的数量 nsmallfree [_NumSizeClasses]uint64 // 小对象释放的数量 arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena//arenas数组集合,管理各个heapArena allArenas []arenaIdx //所有arena序号集合,可以根据arenaIdx算出对应arenas中的哪一个heapArena sweepArenas []arenaIdx //扫描周期开始时allArenas的一个快照 //各个规格的mcentral集合 central [numSpanClasses]struct { mcentral mcentral pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } spanalloc fixalloc // span*的内存分配器(只是分配空结构) cachealloc fixalloc // mcache*的内存分配器 treapalloc fixalloc // treapNodes*的内存分配器 specialfinalizeralloc fixalloc // specialfinalizer*的内存分配器 specialprofilealloc fixalloc // specialprofile*的内存分配器 speciallock mutex // lock for special record allocators. arenaHintAlloc fixalloc // allocator for arenaHints } ``` 通个看这个结构,可以感觉到mheap相对复杂一些,重要字段我已经在代码中注释,我们知道每个golang程序启动时候会向操作系统申请一块虚拟内存空间,仅仅是虚拟内存空间,真正需要的时候才会发生缺页中断,向系统申请真正的物理空间,在golang1.11版本以后,申请的内存空间会放在一个heapArena数组里,由arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena表示,用于应用程序内存分配,下面展示一下数组中一块heapArena虚拟内存空间区域分配, ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/FBE26EAFEB8743F59DE9F89CEE5B3732/2745) 分为三个区域,分别是: - spans区域:存放span指针地址的地方,每个指针大小是8Byte - bitmap区域:用于标记arena区域中哪些地址保存了对象, 并且对象中哪些地址包含了指针,主要用于GC - arena区域:heap区域,程序内存分配的地方,管理的最小基本单位是页,golang一个page的大小是:8KB 下面看一下每个区域的大小情况 heapArena结构体如下: ```go type heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan pageInUse [pagesPerArena / 8]uint8 pageMarks [pagesPerArena / 8]uint8 } ``` 关键字段计算定义如下: ```go heapArenaBytes = 1 << logHeapArenaBytes logHeapArenaBytes = (6+20)*(_64bit*(1-sys.GoosWindows)*(1-sys.GoosAix)) + (2+20)*(_64bit*sys.GoosWindows) + (2+20)*(1-_64bit) + (8+20)*sys.GoosAix // heapArenaBitmapBytes is the size of each heap arena's bitmap. heapArenaBitmapBytes = heapArenaBytes / (sys.PtrSize * 8 / 2) pagesPerArena = heapArenaBytes / pageSize ``` 通过源码可以看出spans大小等于arenaSize/8KB,可以理解为有多少page就准备出对应数量的“地址格子”,来充分保证能存下所有的span地址。 对于bitmap区域,由于bitmap是用来标记每个地址空间的使用情况,我们知道指针大小是8Byte,因此需要arenaSize/8个,一个bitmap可以标记四个地址,因此再除4。 ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/2DADFDB80EEE4C078EC64469E874D887/2746) 如上图所示,是bitmap区域一个字节对arena区域的标记情况情况,高四位标记四个内存地址使用情况,低四位标记存储的是否是指针。对于arenaSize,根据源码公式,在64位非windows系统分配大小是64MB,windows 64位是4MB。 介绍完三个区域,我们再来看一下central [numSpanClasses],它就是管理的所有规格mcentral的集合,同样是134种,pad对齐填充用于确保 mcentrals 以 CacheLineSize 个字节数分隔,所以每一个 MCentral.lock 都可以获取自己的缓存行。而fixalloc类型的相关成员都是用来分配span、mache等对象的内存分配器,这里大家不要搞晕,具体来讲,以span举例,每一个span也需要空间存储,这个就是在spanalloc这个二叉树堆上存储,拿到这个对象,将startAddr 指向arena区域内的npages的内存空间才是给mcache使用的,或者说给P进行对象分配的。另外,由于mheap也是公共资源,一定也要有锁的存在。 下面结合图看一下: ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/D75A017830224B2BA8AF9002EE59DA5E/2744) 从上图可以更清楚的看到,一个mheap会有134种mcentral,而每一种规格的mcentral会挂载该规格的mspan链表。 前面我们讲过tiny对象和小对象的内存分配,那大于 32KB 的对象怎么办呢?golang将大于32KB的对象定义为大对象,直接通过 mheap 分配。这些大对象的申请是以一个全局锁为代价的,所以同时只能服务一个P申请,大对象内存分配一定是页(8KB)的整数倍。结合源码再看一下: ```go func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan { if size+_PageSize < size { throw("out of memory") } //页的整数倍 npages := size >> _PageShift if size&_PageMask != 0 { npages++ } deductSweepCredit(npages*_PageSize, npages) //创建spanclass=0的mspan,从mheap开辟页空间 s := mheap_.alloc(npages, makeSpanClass(0, noscan), true, needzero) if s == nil { throw("out of memory") } s.limit = s.base() + size heapBitsForAddr(s.base()).initSpan(s) return s } ``` 可以看出不管多大对象,一切的空间都是从mheap获取的,那mheap要是不足了呢?就只能向操作系统申请了。 ### 内存分配规则 讲完内存管理组件,我们再来总结一下内存分配规则: - tiny对象内存分配,直接向mcache的tiny对象分配器申请,如果空间不足,则向mcache的tinySpanClass规格的span链表申请,如果没有,则向mcentral申请对应规格mspan,依旧没有,则向mheap申请,最后都用光则向操作系统申请。 - 小对象内存分配,先向本线程mcache申请,发现mspan没有空闲的空间,向mcentral申请对应规格的mspan,如果mcentral对应规格没有,向mheap申请对应页初始化新的mspan,如果也没有,则向操作系统申请,分配页。 - 大对象内存分配,直接向mheap申请spanclass=0,如果没有则向操作系统申请。 流程图如下: ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/E282237A583D4C33A1ACD03E7938202D/2756) 部分内存申请源码源码如下: mcache向mcentral申请,调用go1.12.6\src\runtime\mcache.go refill方法 ```go func (c *mcache) refill(spc spanClass) { // 返回当前的mspan. s := c.alloc[spc] //判断是否所有元素都配分配了 if uintptr(s.allocCount) != s.nelems { throw("refill of span with free space remaining") } if s != &emptymspan { // Mark this span as no longer cached. if s.sweepgen != mheap_.sweepgen+3 { throw("bad sweepgen in refill") } atomic.Store(&s.sweepgen, mheap_.sweepgen) } // 向mheap同规格的mcentral申请mspan 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") } s.sweepgen = mheap_.sweepgen + 3 c.alloc[spc] = s } ``` mcentral空间不足,向mheap申请分配页创建新的mspan,调用go1.12.6\src\runtime\mcental.go grow方法 ```go func (c *mcentral) grow() *mspan { npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) size := uintptr(class_to_size[c.spanclass.sizeclass()]) n := (npages << _PageShift) / size //mheap管理的是page s := mheap_.alloc(npages, c.spanclass, false, true) if s == nil { return nil } p := s.base() s.limit = p + size*n //初始化mspan各变量默认值 heapBitsForAddr(s.base()).initSpan(s) return s } ``` mheap空间不足会调用go1.12.6\src\runtime\mheap.go grow方法进行系统申请 ```go func (h *mheap) grow(npage uintptr) bool { //左移8KB,获取空间大小 ask := npage << _PageShift //系统调用 v, size := h.sysAlloc(ask) if v == nil { print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)\n") return false } //将heap.free二叉树堆里面释放一些无用对象的页pages,平衡一下新申请的空间 h.scavengeLargest(size) //创建一个mspan进行管理使其处于free以至右侧临近span能够发生合并 s := (*mspan)(h.spanalloc.alloc()) s.init(uintptr(v), size/pageSize) h.setSpans(s.base(), s.npages, s) //span的sweepgen = 全局sweepgen,代表已经被sweep过,防止被gc回收 atomic.Store(&s.sweepgen, h.sweepgen) // 设置新span处于在用状态,防止被回收 s.state = mSpanInUse h.pagesInUse += uint64(s.npages) //进行合并 h.freeSpanLocked(s, false, true, 0) return true ``` ### gc改进 通过上节流程图和代码,我们可以清晰的知道一个对象内存申请的整个过程,那思考一下这个流程是否完善,我们都知道golang通过gc进行垃圾回收,而完整的gc需要两次stop the world,如果我们完全依赖gc去垃圾回收是不是影响整个程序的性能,我们假设一个场景,mcentral的span一直不够用,那会不断的去向mheap去申请page空间,导致mheap的使用率很快就触发到gc的阈值,启动gc处理过程,频繁的gc就会导致频繁的程序停服,极大的会影响程序服务性能,那golang的做法是怎么样的呢?,在1.12版本里面golang对mheap结构添加了reclaimCredit 成员变量,每次mcentral向mheap申请新的page空间创建span的时候,都会先去扫描arenas里面的heapArena,去清理垃圾对象回收相同page数量的空间,由于扫描到的垃圾对象不可能正好等于相同page,多清理的page大小就会存到到reclaimCredit里面,下一次再扫描arenas的时候会先去抵消reclaimCredit,如果不够才会去扫描heapArena。通过这种方式有效的防止mheap使用率过快增长,下面是整个流程图: ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/7E95BEC39A2D47708734EE4812B8BB75/2515) 同理,我们知道当mheap不够用的时候,会去向操作系统申请内存空间,如果增长过快,也会造成整个操作系统的不稳定,golang对这部分也做了处理,1.12版本mheap引入scavengeCredit 这个成员变量,当向操作系统申请内存空间的时候,会先去扫描free这个二叉树堆,span从大到小的扫描,释放所需大小的空间给os,多余释放的到小会存储到scavengeCredit中,下次再次扫描的时候会先扣除这个值。下面是整个流程图: ![img](https://note.youdao.com/yws/public/resource/f843698db192fbbc887252f64d2214bd/xmlnote/DC13FFD66E774BE8816E7152F8E4DA7F/2552) ## 结尾 到此也就基本讲完了golang的内存分配的整个环节,本文也是受php内存管理启发,进行了一下golang源码深入研究,由于篇幅关系并没有把源码中的各种细节进行详细讲解,仅对整体流程进行梳理和阐述,对关键源码进行注释和解释,希望能给对golang感兴趣的伙伴给予一定帮助,如需更具体的了解,可以根据这个大流程进行源码学习。本人也是基于1.12.6版本源码一点点梳理出来的,可能在一些细节上会存在出入,也希望golang大佬们对不足的地方给予指正。 本文作者:意随行,原文链接:https://note.youdao.com/ynoteshare1/index.html?id=f843698db192fbbc887252f64d2214bd&type=note

入群交流(和以上内容无关):Go中文网 QQ 交流群:798786647 或加微信入微信群:274768166 备注:入群;关注公众号:Go语言中文网

445 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传