人工回收

c语言由开发者来手动分配与回收内存,调用malloc或calloc分配内存,调用free清空这块内存。
手动回收会存在内存泄漏问题,即分配后没有释放。

自动管理

动态内存管理,即垃圾回收。
优点:
1.增加安全
2.跨系统
3.更少代码
4.运行时校验
5.数组边界检查

程序运行时对象存储在两个位置,堆和栈。栈存储函数值,堆存函数外部引用的值。

垃圾回收

并发标记回收(Concurrent Mark Sweep CMS)算法

GC算法有基于复制的,也有基于分区的。为什么Go选择CMS算法,原因亮点:

  • Go的内存分配策略缓解了CMS容易产生内存碎片的缺陷

    • Go的GC算法采用TCMalloc分配器的内存分配思路,降低碎片率。
  • Go有值类型数据,struct类型。

    • 有了值类型,编译器就不用关心函数间的逃逸分析,只关注函数内的逃逸分析,由此可以将生命周期短的对象在栈上分配。

TCMalloc的分配思想

TC是thread cache的缩写,核心思想:TCMalloc会给每个线程分配一个thread-local cache,用于线程的分配。避免了多线程操作,所以不需要加锁,减少锁竞争带来的性能损耗。

当t-l cache空间不足时,才会向下一级内存管理器请求新的空间。
TCMalloc引入了 Thread cache、Central cache 以及 Page heap 三个级别的管理器来管理内存,可以充分利用不同级别下的性能优势。TCMalloc 的多级管理机制非常类似计算机系统结构的内存多级缓存机制。
TCMalloc缓存.png

在Go的内存管理机制,有几个重要的数据结构,mspan、heapArena、mcache、mcentral以及mheap。

  • mspan和heapArena维护了Go的虚拟内存布局;
  • mcache、mcentral 以及 mheap 则构成了 Go 的三层内存管理器。

虚拟内存布局

  • mspan

    • Go的内存管理基本单元是mspan,每个mspan维护一快连续虚拟内存空间,内存起始地址由startAddr记录。
    • 每个mspan存储的内存空间都是内存页的整数倍,由npages保存。Go的内存页是8KB。

mspan结构如下:


type mspan struct {
    next *mspan     // next span in list, or nil if none
    prev *mspan     // previous span in list, or nil if none

    startAddr uintptr // address of first byte of span aka s.base()
    npages    uintptr // number of pages in span
    ...
    spanclass   spanClass     // size class and noscan (uint8)
    ...
}
  • heapArena

    • heapArena相当于Go的内存块,x86-64架构的Linux系统上,一个heapArena维护的内存空间是64MB。
    • heapArena记录了mspan数组,长度为pagesPerArena,等于ArenaSize/PageSize,用来管理每一个内存页。
    • 整个heapArena的内存基址由zeroedBase记录。

heapArena结构如下:


type heapArena struct {
    ...
    spans [pagesPerArena]*mspan
    zeroedBase uintptr
}

三层内存管理器

  • spanClass

    • 在Go的三级内存管理中,维护的对象都是小于32KB的小对象,并按照大小分为67类,称为spanClass。
    • 每一个spanClass都用来存固定大小的对象。代码定义在runtime.sizeclasses中可以看到。
    • 对于代码中的注释,以 class 3 为例做个介绍。class 3 是说在 spanClass 为 3 的 span 结构中,存储的对象的大小是 24 字节,整个 span 的大小是 8192 字节,也就是一个内存页的大小,可以存放的对象数目最多是 341。
    • tail waste 这里是 8 字节,这个 8 是通过 8192 mod 24 计算得到,意思是,当这个 span 填满了对象后,会有 8 字节大小的外部碎片。而 max waste 的计算方式则是 [(24−17)×341+8]÷8192 得到,意思是极端场景下,该 span 上分配的所有对象大小都是 17 字节,此时的内存浪费率为 29.24%。
    • 以上 67 个存储小对象的 spanClass 级别,再加上 class 为 0 时用来管理大于 32KB 对象的 spanClass,共总是 68 个 spanClass。这些数据都是通过在 runtime.mksizeclasses.go 中计算得到的。我们从上边的注释可以看出,Go 在分配的时候,是通过控制每个 spanClass 场景下的最大浪费率,来保障堆内存在 GC 时的碎片率的。

// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8
//     4         32        8192      256           0     21.88%         32
//    ...
//    67      32768       32768        1           0     12.50%       8192
  • mcache

    • mcache是Go的线程缓存,对应TCMalloc的Thread-Local cache结构。
    • mcache与线程绑定,每个goroutine申请内存时,都不会与其他goroutine发生竞争。
    • mcache会维护长度为numSpanClasses(68*2)的mspan数组,其中包含scan和noscan队列。
    • mcache结构如下:

type mcache struct {
    ...
    tiny       uintptr
    tinyoffset uintptr
    tinyAllocs uintptr

    alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
    ...
}
  • mcentral

    • 当mcache需要扩容时,需要向mcentral申请,mcentral对应于TCMalloc的Central Cache结构。
    • mcentral也维护固定一种spanClass的mspan。
    • 另外还有两个span集合,partial 里存放的是包含着空闲空间的 mspan 集合,full 里存放的是不包含空闲空间的 span 集合。这里每种集合都存放两个元素,用来区分集合中 mspan 是否被清理过
    • mcentral 不同于 mcache,每次请求 mcentral 中的 mspan 时,都可能发生不同线程直接的竞争。因此,在使用 mcentral 时需要进行加锁访问,具体来讲,就是 spanSet 的结构中会有一个 mutex 的锁的字段。

mcentral结构如下:


type mcentral struct {
    spanclass spanClass
    partial [2]spanSet // list of spans with a free object
    full    [2]spanSet // list of spans with no free objects
}

mheap和mcentral.png

  • mheap

    • mheap是Go的运行时的只有一个实例的全局变量。
    • mheap对应TCMalloc中的Page heap结构。
    • heapArena的二维数组就在mheap中。
    • mheap 中存放了 68×2 个不同 spanClass 的 mcentral 数组,分别区分了 scan 队列以及 noscan 队列。

type mheap struct {
    lock  mutex

    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }
}

var mheap_ mheap

Go三级内存管理器.png

对象分配机制

  • 对象三个级别:

    • 微小对象:0-16字节的非指针类型对象
    • 小对象:16-32KB的对象以及小于16字节的指针对象
    • 大对象:spanClass为0的对象。
  • 具体分配策略:

    • 微小对象会被分配到spanClass为2的mspan中,最大16字节,分配时会尽量将微小对象放到同一个内存块,降低碎片化。
    • 我们在 mcache 中提到的三个字段:tiny、tinyoffset 和 tinyAllocs 就是用来维护微小对象分配器的。tiny 是指向当前在使用的 16 字节内存块的地址,tinyoffset 则是指新分配微小对象需要的起始偏移,tinyAllocs 则存放了目前这个 mcache 中共存放了多少微小对象。
    • 对于小对象的分配,整体逻辑跟 TCMalloc 是保持一致的,就是依次向三级内存管理器请求内存,一旦内存请求成功则返回。
    • 对于大对象的分配,Go 并不会走上述的三次内存管理器,而是直接通过调用 mcache.allocLarge 来分配大内存。allocLarge 会以内存页的倍数大小来通过 mheap_.alloc 申请对应大小内存,并构建起 spanClass 为 0 的 mspan 对象返回。

if size <= maxSmallSize {
    if noscan && size < maxTinySize {
      // 微小对象分配
    } else {
      // 小对象分配
    }
} else {
  // 大对象分配
}

内存回收机制

CMS:三色标记清除算法。
三色标记清除算法分几个阶段

  • 清除终止阶段

    • 这阶段会进行STW(Stop The World),让所有线程进入安全点,若是强制GC的话,还要清除上次GC未清除的mspan。
  • 标记阶段

    • gcphase状态从_GCoff切换到_GCmark,并打开write barrier和mutator assists,将根对象压入扫描队列中,此时所有的mutator还处于STW状态。
    • 重启mutator,此时后台的标记线程和mutator assists可以共同帮助GC进行标记,标记过程中,write barrier 会把所有被覆盖的指针以及新指针都标记为灰色,而新分配的对象指针则直接标记为黑色
    • 标记线程开始根对象扫描,包括所有栈对象、全局变量对象,还有所有堆外的运行时数据结构,扫描完根对象后,标记线程会继续扫描灰色队列中的对象,将对象标为黑色并以此将其引用的对象标灰入队。
    • 由于 Go 中每个线程都有一个 Thread Local 的 cache,GC 采用的是分布式终止算法来检查各个 mcache 中是否标记完成。
  • 标记终止阶段

    • 在标记终止阶段会进行 STW,暂停所有的线程。STW 之后将 gcphase 的状态切换到 _GCmarktermination,并关闭标记进程和 mutator assists。此外还会进行一些清理工作,刷新 mcache。
  • 清除阶段

    • 在开始清除阶段之前,GC 会先将 gcphase 状态切换到 _GCoff,并关闭 write barrier。接着再恢复用户线程执行,此时新分配的对象只会是白色状态。并且清除工作是增量是进行的,所以在分配时,也会在必要的情况下先进行内存的清理。这个阶段的内存清理工作会在后台并发完成。

write barrier

  • write barrier的作用是拦截写操作。CMS和G1算法都使用了write barrier来保证并发标记的完整性,防止漏标。

三色标记算法

  • 白色:还未搜索的对象
  • 灰色:已经搜索,但还未扩展的对象
  • 黑色:已经搜索,已做完扩展的对象。

并发标记中最严重的问题就是漏标。如果一个对象是活跃对象,但它没有被标记,这就是漏标。这就会出现活跃对象被回收的情况。

垃圾收集器

go的垃圾收集器包括两部分,mutator和collector。collector执行垃圾收集逻辑和找到应该释放内存的对象。mutator执行程序代码并将新对象分配给堆。
垃圾收集有两个阶段,标记和清除。

  • 标记阶段。收集器遍历堆并标记不再需要的对象。
  • 清除阶段。删除这些对象。go通过三色标记算法来实现。
    go让所有goroutine通过一个叫stop the world机制来达到垃圾收集的安全点。

三色法:
根对象为灰色,其他所有对象为白色,当扫描一个堆栈,

标签: none

添加新评论