golang GC

214 阅读8分钟

GC

Garbage Collection 垃圾回收,自动内存管理的机制;当程序申请的内存不再被需要时,回收内存复用,获取还给操作系统;针对内存级别资源的自动回收过程,就是垃圾回收,负责垃圾回收的组件就是垃圾回收器;为什么需要垃圾回收,人力有限,避免内存泄露问题。牺牲一部分性能,提高程序的可用性,可靠性和稳定性;

垃圾回收器的执行过程被划分为两个独立的组件:

  • 赋值器(Mutator):用户态代码,对于垃圾回收器来说,用户态代码只会修改,增加对象之间的引用关系;
  • 回收器(Collector):负责执行垃圾回收的代码;

根对象(根集合)

指赋值器不需要通过其他对象就可以直接访问到的对象,通过Root对象, 可以追踪到其他存活的对象;

  • 全局变量:编译器就确定存在整个程序生命周期的变量;
  • 执行栈:每个goroutine都有自己的执行栈,栈上包含栈变量和指向堆的指针;
  • 寄存器:寄存器的值可能表示一个指针,参与计算的指针可能指向某些赋值器分配的堆内存区块;

常见的GC实现方式

引用计数式GC

每个对象自身包含一个被引用的计数器,当计数器为0时自动得到回收,方法缺陷较多(循环引用),追求高性能通常不被使用,Python,OC等使用引用计数方式的GC;

实现简单,实时性强;

追踪式GC

核心:可达性即为可用性

一类基于"可达性"分析来进行内存回收的算法, 从根对象触发,通过追踪对象的引用关系,逐步找到所有能够被访问到的对象,这些对象被认为可存活,其他未被访问到的对象,被认为是垃圾,可回收;

标记清除 mark-swap

分为标记和清除两个阶段。标记阶段从根对象开始,将确定存活的对象进行标记;清除阶段遍历整个堆内存,回收未被标记的对象;会产生大量的内存碎片;

标记压缩 mark-compact

和标记清除类似,但是清除部分不是直接清除未标记对象,而是将存活的对象压缩到内存的另一端,更新引用,再回收剩余的内存。避免了内存碎片,但是对于大对象的移动成本较高;

分代收集 generational

根据对象的生命周期不同,将内存划分为新生代和老年代,可以针对不同代的特点采用最适合的算法,提高垃圾回收的效率。缺点就是管理复杂,需要合理划分不同代的大小以及不同代的晋升策略。

新创建的对象放入新生代,将经过一定次数GC(策略)仍然存活的新生代对象移动到老年代。

复制 copying

将内存分为两块,新分配的对象放在其中一块内存中,当这块内存满了,就从根对象开始,复制所有可达的对象到另一块内存中,然后回收原来的内存区域。

避免了内存碎片,但是需要两倍的内存空间。主要适用于频繁分配和回收对象的场景,可以减少碎片的产生。

增量和并发 incremental concurrent

对于上述的优化,目的是减少STW(stop the world)的时间,降低GC对于程序运行的影响;

增量垃圾回收是将垃圾回收的工作分解成多个小步骤,交错在程序运行中执行;并发垃圾回收则是让垃圾回收和程序运行在不同的线程中并发执行。

Golang的垃圾回收算法

三色标记法

三色标记法的关键是理解对象的三色抽象以及波面(wavefront)推进这两个概念。三色抽象只是一种描述追踪式回收器的方法,在实践中并没有实际含义,它的重要作用在于从逻辑上严密推导标记清理这种垃圾回收方法的正确性。也就是说,当我们谈及三色标记法时,通常指标记清扫的垃圾回收。

三色抽象(将对象的状态抽象为三种状态)

  • 白色对象(可能死亡):未被回收器访问到的对象。回收开始阶段,所有对象都为白色,回收结束阶段,所有白色对象为不可达对象即垃圾;
  • 灰色对象(波面):已经被回收器访问到的对象,但回收器需要对于其中一个/多个指针进行扫面,因为他们可能还指向存活的白色对象;
  • 黑色对象(确认存活):已经被回收器访问到的对象,其中的每个字段都已经被扫描,黑色对象中任何一个指针对象都不可能直接指向白色对象;

三种不变性所定义的回收过程其实是一个波面不断前进的过程,这个波面同时也是黑色对象和白色对象的边界,灰色对象就是这个波面。

three-color.png

如果单纯执行三色标记法的GC,会出现因为标记过程中,对象引用变化导致的对象被错误的回收等各种问题。需要借助STW进行保证GC的正确执行;

STW的过程有明显的资源浪费,对所有的用户程序都有很大影响;

总结对象丢失,需要同时满足以下条件:

  • 白色对象被黑色对象引用
  • 灰色对象和白色对象之间的可达关系被破坏

屏障机制

为了避免STW,需要破坏导致对象丢失的条件,引入两种限制条件分别针对上面丢失的情况:

  • 强三色不变式:黑色对象不能引用白色对象,不存在黑色对象引用到白色对象的指针。
  • 弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态。

插入写屏障(强三色不等式):当一个对象引用另外一个对象时,将另外一个对象标记为灰色。

不会存在黑色对象引用白色对象的情况;v1.5 插入写屏障仅会在堆内存中生效,不对栈空间生效,这是因为go在并发运行时,大部分的操作都发生在栈上,函数调用会非常频繁。数十万goroutine的栈操作都进行屏障保护自然会有性能问题。

GC都是针对堆内存的活动,对象分布位置在栈和堆上,因为函数调用弹出的频繁使用,所以 插入屏障 机制,在栈空间的对象操作中不使用,只在堆空间对象的操作中;

过程:

insert-sheld.png

优点:

保证标记算法和用户代码的并发执行

缺点:

由于栈上的对像没有插入写机制,在扫描完成后,仍然可能存在栈上的白色对象被黑色对象引用,所以在最后需要对栈上的空间进行STW,防止对象误删除。

在一次正常的三色标记流程结束后,需要对栈上重新进行一次stw,然后再rescan一次。

删除写屏障(弱三色不等式): 在删除引用时,如果被删除引用的对象自身为灰色或者白色,那么被标记为灰色。

白色对象始终会被保护,灰色对象到白色对象的路径不会断;v1.5

  1. 删除写屏障也叫基于快照的写屏障方案,必须在起始时,STW 扫描整个栈(注意了,是所有的 goroutine 栈),保证所有堆上在用的对象都处于灰色保护下,保证的是弱三色不变式;
  2. 由于起始快照的原因,起始也是执行 STW,删除写屏障不适用于栈特别大的场景,栈越大,STW 扫描时间越长
  3. 删除写屏障会导致扫描进度(波面)的后退,所以扫描精度不如插入写屏障;

如果不起始扫描所有栈,可能出现丢数据(栈上对象不触发屏障)

过程:

delete-sheld.png

优点:

保护对象不丢失

缺点:

一个对象的引用被删除后,即使没有其他存活的对象引用它,它仍然会活到下一轮。产生很多的冗余扫描成本,且降低了回收精度。

混合写屏障:插入屏障和删除屏障进行混合,尽可能减少 STW 的时间。

核心定义:

  • GC刚开始时,将栈上的所有可达对象标记为黑色
  • GC期间任何在栈上创建的对象都为黑色
    • 不需要对栈上的对象,stop the world,保证栈上的对象不会丢失
  • 堆上被删除的对象标记为灰色
  • 堆上新增加的对象标记为灰色

混合写屏障扫描栈虽然没有 STW,但是扫描某一个具体的栈的时候,还是要停止这个 goroutine 赋值器的工作(针对一个 goroutine 栈来说,是暂停扫的,要么全灰,要么全黑哈,原子状态切换);

GC触发的情况

  • 内存分配量达到阈值触发GC
    • 当程序使用的内存超过一定阈值时,垃圾回收器会被触发以回收不再使用的内存空间。
  • 定期触发GC
    • 使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。
  • 手动触发GC
    • runtime.GC() 函数时,可以强制执行一次垃圾回收操作