golang 内存分配和学习笔记

Posted by Jason on Saturday, August 3, 2019

TOC

golang 垃圾回收机制学习

常用回收算法

目前常用的垃圾回收算法,主要有:引用计数算法、标记-清除算法、标记-整理算法、分代算法、复制收集算法。

  • 引用计数法:
    • 缺点:循环引用造成内存泄漏、实现复杂
  • 标记-清除:
    • 缺点:造成内存碎片;
  • 标记-压缩:
    • 缺点:压缩时会造成延时;
  • 标记-复制:
    • 缺点:空间利用率低,需要两块空间;

java GC 实现

Java 将内存分为三个区域:新生代、老年代、持久代(方法区)

新生代分为:Eden、survivor(包含两个区from、to)

  • Eden 没有空间可以分配时,需要执行一次 minor GC,将 Eden 区和survivor from 区 copy 到 survivor to 区(标记-复制)。如果 survivor 中对象复制次数大于15或者 survivor 区域剩余空间小于50%,则将该对象升级到老年代。
  • 如果老年代引用新生代,这个时候就会漏掉某写新生代对象。针对这个问题,hotspot 中引入了 CardTable 用于标记某个老年代对象是否引用了新生代。在 jit 编译器中通过写屏障来实现。

java 垃圾回收器

  • 堆内存分代后,会根据他们的不同特点来区别对待,进行垃圾回收的时候会使用不同的垃圾回收方式,针对新生代的垃圾回收器有如下三个:Serial、Parallel Scavenge、Parallel New,他们采用的都是标记-复制的垃圾回收算法。
  • 针对老年代的垃圾回收器有如下三个:Serial Old 、Parallel Old 、CMS,他们使用的都是标记-压缩的垃圾回收算法。

  • 参考1

  • 参考2

  • gc调优

go long

golang 内存分配

golang 中内存管理使用了 TCMalloc 算法。将内存划分为三个区:span、bitmap、arena。

  • golang 中内存的基本单位是page,一个 page 对应8KB内存。另外将系统内存分成67种span,每一个 span 对应一种内存大小,比如:索引为 3 的内存class 可以分配给 17B-32B 大小的对象,其中最大分类大小为32KB,超过这个大小的对应 0 class ;

    var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144....
    
  • 在每个协程(P)中,会缓存申请的 span,保存在 mcache 结构中,因为每个协程中都不是并发的,所以申请内存时不需要加锁;

type mcache struct {
    alloc [numSpanClasses]*mspan
}
  • 在 mcache 上层是 mcentral 对象,它是存储在 mheap 对象中,存在多个协程竞争的情况。它内部有两个span 列表列表,一个 span class 对应一个 mcentral ,所以申请不同 class 大小的内存并不会产生锁竞争。
type mcentral struct {
    lock      mutex
    empty     mSpanList
    nonempty  mSpanList
}
  • 在 mcentral 上层是 mheap 类型,它是一个全局变量,负责所有span的分配和切分。内部也存在一个全局锁。
type mheap struct {
    lock      mutex
    allspans  []*mspan
    central   [numSpanClasses]*mspan{
        mcentral mcentral
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte //内存对其
    }
}                                                    }

分配流程:

  1. 32KB 的对象,直接从mheap上分配;
  2. <=16B 的对象使用mcache的tiny分配器分配;
  3. (16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
  4. 如果mcache没有相应规格大小的mspan,则向mcentral申请;
  5. 如果mcentral没有相应规格大小的mspan,则向mheap申请;
  6. 如果mheap中也没有合适大小的mspan,则向操作系统申请

gc过程

什么时候会进行垃圾回收:

  1. 主动强制调用 runtime.GC() 函数;
  2. 超过一段时间没有进行过GC;
  3. 内存超过阈值;

go 对三色标记法进行了优化,只在标记根扫描和 rescan 时才会进行 STW,并且标记阶段每个P中会创建一个 Background Mark Wroker 进行标记。在标记过程中会发生对象引用的变化,Golang 中使用了写屏障,会记录引用的变化。另外,新申请的对象内存会标记为黑色,会在下一次GC的时候进行扫描。

  • 首先进行根(全局变量和各个P栈)扫描,将可达的对象标记为灰色,并开启写屏障,开启mutator assist;这个过程中开启了STW;
  • 然后,继续扫描灰色对象引用的子对象,将可达子对象标记为灰色;扫描完母对象后标记为黑色;(这个过程一直进行直到不存在灰色对象为止);
  • 最后,rescan 根对象和写屏障记录的变动对象,然后关闭写屏障;
  • 清理白色集合中剩余的对象(这个时候可以并行进行);

mutator assist

为了防止heap增速太快,在GC执行的过程中如果同时运行的G分配了内存,那么这个G会被要求辅助GC做一部分的工作。

参考

  1. GC 源码剖析
  2. Golang源码探索(三) GC的实现原理
  3. go 内存分配

「真诚赞赏,手留余香」

Jason Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付


comments powered by Disqus