Golang 1.14中内存分配、清扫和内存回收

golang内存分配

Golang的内存分配是由golang runtime完成,其内存分配方案借鉴自tcmalloc。
主要特点就是

  • 为每个工作线程(P)都维护了一个分配cache,小对象((16,32KB])和微(Tiny)对象((1B,16B])基本上无需全局锁。
  • 为常用的内存尺寸(16B,32KB] 之间内存分类为numSpanClass(134-2)种对象尺寸。位于此大小区间的内存分配都归到这些spanclass 尺寸的element上进行分配。如此可以减少内存碎片的产生。

本文中的element指一定大小的内存块是内存分配的概念,并为出现在golang runtime源码中
本文讲述x8664架构下的内存分配

主要结构

Golang 内存分配有下面几个主要结构

  • mspan 用于管理一连串地址连续的页面,golang将同一mspan维护的地址空间划分为同一个尺寸的element,每个element用于存储一个大小属于当前SpanClass对象。
  • mcache 每个P都有一个mcache,在此mcache上有一个长度为numSpanClass的数组,里面存储的每个成员指向一个mspan。
  • mcentral 全局的,每个mcentral用于管理相同的element size的mspan。
  • mheap 全局的,用于管理golang的整个堆。在mheap上有numSpanClass个mcentral数组。
  • heapArena 在x8664上用于管理64MB的堆空间。一个heapArena下有多个mspan。


    图片发自App

内存分配方案

微(Tiny)对象

Tiny对象是指内存尺寸小于16B的对象,这类对象的分配使用mcache的tiny区域进行分配。当tiny区域空间耗尽时刻,它会从mcache.alloc[tinySpanClass]指向的mspan中找到空闲的区域。当然如果mcache中span空间也耗尽,它会触发从mcentral补充mspan到mcache的流程。

小对象

小对象是指对象尺寸在(16B,32KB]之间的对象,这类对象的分配原则是:
1、首先根据对象尺寸将对象归为某个SpanClass上,这个SpanClass上所有的element都是一个统一的尺寸。
2、从mcache.alloc[SpanClass]找到mspan,看看有无空闲的element,如果有分配成功。如果没有继续。
3、从mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合适的mspan,返回给mcache。如果没有找到就进入mcentral.grow()—>mheap.alloc()分配新的mspan给mcentral。

大对象的分配

大对象指尺寸超出32KB的对象,此时直接从mheap中分配,不会走mcache和mcentral,直接走mheap.alloc()分配一个SpanClass==0 的mspan表示这部分分配空间。

总结

对于程序分配常用的tiny和小对象的分配,可以通过无锁的mcache提升分配性能。mcache不足时刻会拿mcentral的锁,然后从mcentral中充mspan 给mcache。大对象直接从mheap 中分配。

进程虚拟地址空间管理

在x8664环境上,golang管理的有效的程序虚拟地址空间实质上只有48位。在mheap中有一个pages pageAlloc成员用于管理golang堆内存的地址空间。golang从os中申请地址空间给自己管理,地址空间申请下来以后,golang会将地址空间根据实际使用情况标记为free或者alloc。如果地址空间被分配给mspan或大对象后,那么被标记为alloc,反之就是free。

地址空间的状态

Golang认为地址空间有以下4种状态:

  • None 地址空间初始状态
  • Reserved 地址已经被golang runtime拥有,但是os并为真正分配,访问此类地址出发异常
  • Prepared 地址已经是Reserved,但是也并为被OS分配真正地址空间。在Linux系统上,Prepared对应的是地址空间为MADV_FREE 表示os可以在自己认为需要的时候回收这段地址空间。
  • Ready 地址空间真真正正被os分配。访问此空间不会出发异常。

Golang同时定义了下面几个地址空间操作函数:

  • sysReserved 调用此函数后,地址从None转换为Reserved状态
  • sysAlloc 地址从None转换为Ready状态,一般都是golang runtime自己内存管理对象的分配使用sysAlloc
  • sysFree 地址空间从任意状态转换为None
  • sysMap 地址空间从Reserved转换为Prepared
  • sysUsed 地址空间从Prepared转换为Ready
  • sysUnused 地址空间从Ready转换为Prepared


golang如何管理自己的虚拟地址空间

在mheap结构中,有一个名为pages成员,它用于golang 堆使用虚拟地址空间进行管理。其类型为pageAlloc

type pageAlloc struct{
...
summary [summacryLevels][]pallocSum
chunks [1<

pageAlloc 结构表示的golang 堆的所有地址空间。其中最重要的成员有两个:

  • summary 为二维数组,组成一个pallocSum的radix tree。在golang 1.14中x8664 radix tree 为4级。最终那一级节点表示一个chunk(2MB)地址空间的分配情况。
    pallocSum定义为64bit 长整型,它被分为三个bitmap:start,max 和end。start表示这段地址空间从起始地址开始连续的free的地址空间的页面数量;max这段地址空间最大连续页的数量,end 为这段地址空间的最后一个页编号。


  • chunks 数组每个成员表示了地址空间内所有chunk里页面分配和scavenge情况。其结构为pallocData,分为两个成员,pallocBits成员表示已分配页面的bitmap,scavenged 成员表示已scavenged的页面的bitmap。
    chunk如前所述在x8664上为2MB,也就是常规的一个巨页页面大小
    当alloc mspan时刻,需要使用pageAlloc,涉及到下面函数的使用:

  • (s* pageAlloc)alloc找到一个地址空间可以容纳待分配的页面数量,并把这段地址空间标记为alloc。

  • (s*pageAlloc)grow 增长mheap管理的堆地址空间

  • (s*pageAlloc)free 释放地址空间(将这段地址空间标记为free)
    alloc mspan时,先使用pageAalloc.alloc尝试分配一段地址空间,如果没有分配成功,就使用grow增加一段地址空间映射,最后再使用alloc分配。

空间清扫sweep

在golang的gc流程中会将未使用的对象标记为未使用,但是这些对象所使用的地址空间并未交还给os。地址空间的申请和释放都是以golang的page为单位(实际以chunk为单位)进行的。sweep的最终结果只是将某个地址空间标记可被分配,并未真正释放地址空间给os,真正释放是后文的scavenge过程。

mspan的sweep

在gc mark结束以后会使用sweep()去尝试free一个span;在mheap.alloc 申请mspan时刻,也使用sweep去清扫一下。
清扫mspan主要涉及到下面函数

  • mspan.sweep()扫描这个span中element的使用情况,当最终如果整个mspan所有的element都释放了,那么使用freeSpan()
  • mheap.freeSpan()释放golang runtime的这个mspan对象,同时将mspan表示的地址空间标记为可分配。

地址空间回收(scavenge)

如上节所述,sweep只是将page标记为可分配,但是并未把地址空间释放;真正的地址空间释放是scavenge过程。
真正的scavenge是由pageAlloc.scavenge()—>sysUnused()将扫描到待释放的chunk所表示的地址空间释放掉(使用sysUnused()将地址空间还给os)
golang的scavenge过程有两种:

  • 同步scavenge,当每次mheap.grow() 增长mheap的内存的时刻,如果增长量达到一定水平就会触发scavenge的扫描过程。在scavenge扫描过程中,golang会尝试释放一定数量的chunk。
  • 后台scavenge,golang内部有定时器和相关的goroutine,定期扫描程序内存使用量,当内存使用量超出一定阈值的时候,也会调用scavenge过程,尝试释放内存给os系统。
  • golang runtime package中定义了 debug.freeOSMemory 会手动触发一次gc,并调用scavengeAll,对mheap管理所有地址空间进行scavenge *

你可能感兴趣的:(Golang 1.14中内存分配、清扫和内存回收)