Go的内存管理以及GC机制 | 青训营笔记

371 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第2天

1.自动内存管理

1.1相关概念

  • Mutator:业务线程,分配新对象,修改对象指向关系
  • Collector:GC线程,找到存活对象,回收死亡对象的内存空间
  • Serial GC:只有一个collector
  • Parallel GC:支持多个collectors同时回收的GC算法
  • Concurrent GC:mutator(s)和collector(s)可以同时执行

image.png

注意:Collector GC的时候,Collectors必须感知对象指向关系的改变。如下图所示:

image.png

当在GC前有o和a两个对象并且o指向了a,如①所示,在GC过程中,标记了o和a两个对象是存活的,如②所示,而在发生回收之前又有一个对象b被对象o指向,但是o和a已经被标记了,所以会造成这个b对象没有被标记到从而可能被回收的错误,如③所示。所以在Concurrent GC的时候,Collectors必须感知对象指向关系的改变

1.2 追踪垃圾回收

进行垃圾回收的时候可分为三个步骤,第一步标记根对象,第二步找到可达对象并标记,第三步清理所有不可达对象

image.png

①标记根对象:将静态变量、全局变量、常量、线程栈等以后会用的变量所指向的对象标记为存活的,如下图

image.png

② 找到根对象出发所有可达的对象,也就是求指针指向关系的传递闭包

image.png

③清理不可达的对象,这里设计几种清理策略

  • Copying GC:将所有存活的对象复制到一块新的内存中去,并清理原来的内存空间
  • Mark-sweep GC:将要被清理的对象的内存标记为“可分配”,这样当下一次内存申请的时候发现该对象被标记成“可分配”,就可以使用这一块内存空间
  • Mark-compact GC:在原本的内存空间上进行整理,将存活的对象整理到该内存空间的某一块地方

image.png

1.3分代GC

每个对象都有年龄,当一个对象经历了一次GC并且没有被回收,它的年龄将会+1,在内存中分为了新生代(Young Generation)和老年代(Old Generation)两个区域,针对不同年龄的对象将其放入不同的区域中

此处需要提及分代假说,分代假说是指每次GC后大多数的对象的声明周期都很短,例如在一个方法中new的对象,方法结束之后该对象就应该被回收了。

image.png

对于不同的区域应该灵活的选用不同的GC算法。

新生代中:

  • 常规的对象分配
  • GC吞吐率高
  • 由于存活对象很少,可以采用copying collection

老年代

  • 对象一直存活,反复复制开销大
  • 可以采用mark-sweep collection

1.4引用计数法

在引用计数法中,每个对象都有一个与之关联的引用数目,当被一个对象被另一个对象引用的时候,其数目+1.

对象存活的条件是当且仅当引用数大于0

如图所示,当前对象为o,指针p指向该对象,指针q指向空,此时o的引用数是1

image.png

接下来把q指向了p,此时就有两个指针引用该对象,其数目为2

image.png

优点

  • 内存管理的操作被平坦到程序执行的过程中
  • 内存管理不需要了解runtime的实现细节

缺点

  • 维护引用技术的开销大:通过原子操作保证对引用计数操作的原子性和可见性
  • 无法回收环形数据结构,因为该环中每个引用数都不小于1
  • 内存开销:每个对象都引入的额外内存空间存储引用数目
  • 内存回收时可能引发暂停

2.Go 内存分配

2.1内存分配--分块

  • 目标:为对象在heap上分配内存
  • 提前将内存进行分块,当请求一个对象的空间时,选择一个与该需求空间最接近的内存空间,快速地进行内存分配
  • 步骤:
    • 调用系统调用mmap()向OS申请一大块内存,例如是4MB

    • 现将内存划分成大块,例如8KB,称作mspan

    • 再讲大块继续划分成特定大小的小块,用于对象分配

    • noscan mspan:分配不包含指针的对象--GC不需要扫描

    • scan mspan:分配包含指针的对象--GC需要扫描

image.png

2.2内存分配--缓存

  • 每个goroutine都绑定了一个p,p包含了一个mcache用于快速分配
  • 在mcache中管理着一组mspan
  • g请求分配内存的时候慧聪mcache中选择一个合适的内存大小用于分配
  • 当mcache中的mspan分配完毕后,会向mcentral申请带有未分配块的mspan,然后将其放入mspan中,并分配一个合适的内存大小
  • 当mspan中没有分配对象时,mspan会被mcache缓存在mcentral中,而不是立刻释放并归还给OS

image.png

2.3内存管理优化

对象分配是非常高频的操作,在大项目中每秒会分配GB级别的内存,在这些对象中小对象占比较高。而由上文可知GO内存分配的路径比较长,g -> m-> p -> mcache -> mspan -> memory block -> return pointer会消耗非常多的时间。所以可以提出一个优化,优化方案的名字叫做Balanced GC。

  • 在Balance GC中,每个g都绑定了一块大内存(1KB),称作 goroutine allocation buffer(GAB)
  • GAB用于noscan类型的小对象分配,小对象是指大小 < 128 B
  • GAB使用三个指针进行维护:base(记录GAB的头地址),top(使用到的地址),end(尾地址)
  • 使用指针碰撞锋哥(Bump pointer)进行对象分配,即当当前对象的大小可以由GAB分配的时候,就将top的地址返回,并将top移动相应的大小的位置。优点是无需和其他分配请求互斥,分配动作简单高效

GAB的示意图: image.png

申请空间的示意图:

image.png

GAB对于go内存管理来说是一个大对象,本质上是将多个小对象的分配合并成一次大对象的分配,仍然是走mcache和mspan那一套。

!!存在的问题:GAB的对象分配会导致内存被延迟释放。!!

例如在一个1KB大小的GAB中有一个 8B 大小的对象,此时在GC的时候就会认为该整个GAB都是存活的,造成了资源的浪费。

解决方案 使用copying GC算法管理小对象。当GAB的总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中,原先的GAB可以释放,避免内存泄漏

image.png