【Go学习】高级特性(二)垃圾回收

142 阅读6分钟

关于垃圾回收的基本概念

了解垃圾回收吗?

英文单词garbage collection,是一种对内存进行自动管理的机制。

当程序所申请的内存不再需要时,GC机制就会自动将这块内存回收交还给操作系统或者重新分配给别的申请内存的程序。

垃圾回收的过程涉及到了哪些组件?简单说说你对它们的理解

  • 赋值器mutator 实际指的是用户态的代码,它修改了对象间的引用关系
  • 回收器collector 执行gc的代码

讲讲垃圾回收的具体流程?

  • SweepTermination 清扫终止阶段,启动写屏障
  • Mark 扫描标记阶段,与赋值器并发执行,写屏障执行
  • MarkTermination 标记终止阶段,停止写屏障
  • GCoff 内存清扫阶段,归还内存至堆,关闭写屏障
  • GCoff 内存归还阶段,归还内存至操作系统,关闭写屏障

听说过根对象/根集合这个概念吗?

根对象是gc在标记过程中最先检查的对象。内容包括:

  • 全局变量
  • 协程私有的执行栈
  • 寄存器 因为寄存器的值也可能是一个指针

常用的垃圾回收算法有哪些?Go用哪一种?

可以分为两大类:追踪式和引用计数式 相结合的综合运用

追踪式

  • 标记清扫 从根节点出发,标记存活的,清扫可回收的(这实际上就是一种搜索算法)
  • 增量式 标记和清扫分批、每次小批量进行,所以叫“增量”。
  • 增量整理 增量式+整理,整理的是对象
  • 分代式 分年轻代、老年代和永久代。以某个值对年轻代和老年代进行区分,永远不会被回收的对象就是永久代。

并且有如下规则:存活时间不长的倾向于被回收;已经存活了很久的倾向于继续存活

引用计数式

设一个count值,表示对象自身的引用计数。当cnt=0时回收。

Go:三色标记清扫算法

Go使用的是三色标记清扫算法

  • 特点:无分代、不整理、并发
  • 原因:
    • 关于“整理”:Go基本上不存在碎片问题;多线程场景下也不需要使用顺序内存分配器

    • 关于“分代”:go有逃逸分析,存活时间短的对象直接被分配到栈上,协程死后被直接回收,不需要gc的参与

    • go团队的目标是更好地让gc与用户代码并发执行,而非仅为了减少停顿时间

关于三色标记法

请简述三色标记法的流程

接下来会从三色是哪三色,以及如何标记两方面展开。

  • 三色指的是黑灰白
  • 这里要提一下另外一个概念:波面推进。
  • 在垃圾回收开始时,所有的节点都还没有被标记,我们称这种未被标记的节点是白色的;当一部分节点被标记了之后,我们可以认为它们形成了一个“波面”(wavefront),但是它们是处在一个中间地带,所以我们说是灰色的,因为它们中的一个或多个指针还有可能指向白色对象);而当一个对象的所有子节点都完成扫描时,会被标记为黑色
  • 最终,仅剩下黑色和白色两类对象,黑色的就是可达对象,是存活的;白色的是不可达对象,可能死亡,可以回收。

更简单的说法就是:从根节点开始,扫描一遍,标记成灰色,再扫一遍标记为黑色。

可以并发地执行标记和清除的步骤吗?

可以。但难点在于如何保证标记与清除过程的正确性。

了解写屏障、混合写屏障吗?

这是一个在并发垃圾回收器中才会出现的概念。实际指的是赋值器的写屏障,这是一种同步机制,使得赋值器在进行指针写操作时,能够“通知”回收器,进而不会破坏弱三色不变性。

什么是若三色不变性呢?

其实三色不变性有强弱两个概念。

垃圾回收的正确性体现在:就是既不杀错(错把黑色的给回收了),也不放过(漏掉了白色对象)。

如果同时满足以下两个条件,就破坏了这种正确性:

  1. 黑色对象引用白色对象(放过了白色对象)
  2. 灰色对象到达白色对象的、未被访问的路径被破坏
  • 所谓的弱不变性,就是可以允许情况1发生,保证情况2不发生;
  • 相应地,强不变性就是保证两种情况都不发生。

经典的写屏障有哪些?

  • Dijkstra插入屏障 破坏条件1
func WritePointer(slot *unsafe.Pointer,ptr unsafe.Pointer){
  //将ptr标记为灰色
  shade(ptr)
   *slot=ptr
}
  • Yuasa删除屏障 破坏条件2
func WritePointer(slot *unsafe.Pointer,ptr unsafe.Pointer){
  //将*slot标记为灰色
  shade(*slot)
   *slot=ptr
}

了解STW吗?

这个s可以有两种理解:start/stop;后面的TW自然就是the world了。

它指的是一种暂停的状态,暂停的是赋值器进一步操作对象图的过程。 GC在需要进入STW前,会通知所有的用户态代码停止。

(如果有协程不停止,就一直无法进入STW,go1.14之后这类协程可被异步抢占,不太需要担心这个问题)

有了垃圾回收之后,还会发生内存泄露吗?

会的。

内存泄露是指预期被回收的内存没能得到 及时 的回收。

在Go中:

  • 该内存被根对象引用 将某个变量附着在了全局对象上,且忽略了释放
  • 协程泄露 协程需要维护执行用户的上下文信息,这需要消耗一定内存,且这部分在协程不结束前是不会被回收的。如果一直创建新协程而不结束旧协程就会有问题。

了解触发垃圾回收的时机吗?

  • 主动触发 在Go中通过runtime.GC来触发
  • 被动触发
    • 通过系统监控,比如超过2分钟没有GC则强制触发GC
    • pacing算法

谈谈pacing算法

以下是pacing模型所关注的问题:

image.png

  • 目标就是估计trigger,使得actually和goal最为接近;
  • 理想情况下,用户代码满载时,GC的cpu利用率不应超过25%

如果内存分配速度超过了标记清除的速度怎么办?

并发标记有一个标志位,在调用内存申请时会进行检查,暂停分配内存过快的的协程,转去执行一些辅助标记的工作。

垃圾回收关注哪些指标?

  • CPU利用率
  • GC停顿时间
  • 停顿频率
  • 可拓展性

如何调优?

三大关键词:控制、减少、复用

  • 优化内存的申请速度,提高赋值器的CPU利用率
  • 尽可能少地申请内存;复用已经申请的内存
  • 手动增大GOGC的值,推迟出发时间,从而减少触发频率

参考文献

  • 《Go面试笔试宝典》