Go内存收集(GC)1

168 阅读5分钟

这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战

主要有两种收集方式:

  • 手动管理:C、C++、Rust,由工程师手动控制
  • 自动管理: Python、Ruby、Java、Go
    • 类似stop the word

上一节了解了内存分配器,这次来了解一下垃圾收集器,回收堆上的内存空间

分配与回收,主宰了整个内存空间的生与死

设计原理

要回收的不一定是垃圾,但是一定是不要的东西,如何判断其是否不要?

标记清除

也即Mark-Sweep,跟踪式垃圾收集器,标记着不再需要的东西,再将其对应的空间加入到空闲链表,对象不是不在了,而是没法再被找到

分为两个阶段进行清除:

  1. 标记,任何东西的存在都是有痕迹的,程序也不例外,在被使用的对象,会被程序级级引用,从根对象出发,标记一路上使用到的东西
  2. 清除,遍历堆中所有的对象,没被标记的都会被清除,加入到空闲空间的链表

如果stop the word,是没有办法完全进行标记的

栈上的对象也是根对象

三色抽象

多数收集器都会使用三色抽象,缩短长时间的STW,只有三种颜色:

  • 黑:活跃对象,没有引用外部指针或者根对象可达的对象
  • 白:潜在的垃圾,可能会被回收
  • 灰:活跃对象,存在指向白色的指针,收集器会进行这些对象的扫描

一开始还没有颜色,收集器不知从何开始扫描,因此根对象为灰,三个步骤:

  1. 从灰集合中开始,标记为黑
  2. 将所有黑指向的对象标记为灰
  3. 重复直到世上不再有灰

完成后就只剩黑和白,收集器回收掉白

仍旧需要STW,如果用户在回收期间改变了对象指针就难以标记一些新增的对象引用导致错误回收

无法实现并发、增量回收

屏障技术

一种屏障指令

  • CPU以及编译器执行内存相关操作的时候遵守特定的约束
  • 保证一定的顺序性,屏障前执行的指令一定先于屏障后执行的指令

并发、增量回收的条件:下面二选一

  • 强三色不变性:黑不会指向白,只会指向灰、黑
  • 弱三色不变性:黑可指向白,但被指向的白必然存在另外一条路径是从灰开始经过多个白到达的

屏障技术通过维护上述不变性来实现并发、增量回收

类似钩子函数,在程序进行对象创建、更新、读取的时候会执行的代码,这多出来的代码会影响执行效率

两种屏障:

  • 写屏障
  • 读屏障

go使用的是写屏障:

  • Dijkstra的插入写屏障
  • Yuasa的删除写屏障

是指针对程序对对象引用关系的操作,如何改变关联对象的颜色 0

这里面要论证两个事情:

  • 写屏障如何保证不变性
  • 不变性如何保证并发、增量删除的正确性

参考:golang.design/under-the-h…

插入写屏障

核心思想:避免黑指向白,因此在新对象被黑指之前,需要变成灰

在用户修改对象指向的指针的时候会触发以下代码

writePointer(slot, ptr):
    shade(ptr)
    *slot = ptr

其中shade会修改参数指针为灰色,该对象的后续对象为白色

ptr为白色,会转变为灰色

  • 能够保证改变指向后的对象不会被删除
  • 但是之前的对象由于没有被删除原有的指向关系,最终依旧变成了黑色
  • 较为保守部分不会被使用到的对象也会被标记为黑色
  • 栈上的对象的修改操作都会引起插入写屏障的话,性能很不好
    • 回收过程始终标记栈为恒灰的话需要STW

删除写屏障

在用户修改对象指向的指针的时候会触发以下代码

writePointer(slot, ptr)
    shade(*slot)
    *slot = ptr

shade会把参数原来指向的对象颜色给标记成灰色,该对象的后续对象为白色

  • 适用于栈上对象的回收
  • 由于存在重新标记成灰、白的行为,因此会产生重复的冗余扫描

混合写屏障

go1.8之后采用了如下的混合写屏障

writePointer(slot, ptr):
    shade(*slot)
    shade(ptr)
    *slot = ptr

由于删除写屏障没有对栈开启,可以通过简单的将栈引用白色对象,就可以让白色对象躲过一轮GC

在混合写屏障中,一旦栈变成黑色,那么后续被栈引用的对象也必须为黑色

2016年的时候似乎还未实现

增量与并发

长时间STW对于实时的引用往往是难以接受的,可以通过以下两个方面对收集器进行优化

  • 增量收集:增量标记、清除垃圾
  • 并发收集:并发收集清除垃圾

需要以下几个东西来保证

  • 写屏障来保证正确性
  • 并不是等到内存溢出才进行回收

并发、增量收集

保证三色不变性时,可以保证正确性

  • 打开写屏障
    • 一定程度降低程序效率
  • 增量GC中新增的GC也是会有时间消耗的