这是我参与「第五届青训营」笔记创作活动的第十三天
Go堆内存垃圾收集机制
设计原理
标记清除
标记清除(Mark-Sweep)算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:
- 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象;
- 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表;
整个过程需要标记对象的存活状态,用户程序不能执行分配对象,需要STW。
三色标记
实现三色标记算法可以减少STW的时间,三色标记算法把对象分成白色、灰色、黑色三种对象:
- 白色对象:初始时,所有对象都为白色,表示它们从来没被扫描过,也未标示为存活对象
- 灰色对象:表示对象已经被扫描过,但其引用的对象还未被扫描。灰色对象在标记过程中,可能会产生新的灰色对象,直到所有灰色对象的引用被扫描完
- 黑色对象:表示对象已经被扫描过,并且其引用的对象也被扫描过,因此可以视为存活对象。所以黑色对象不会被回收
过程:初始时,所有的对象都会被标记为白色。垃圾收集器会从根对象开始,遍历它们引用的所有对象,并将它们标记为灰色。然后递归地遍历灰色对象的引用,直到没有新的对象可以遍历为止,在此过程中已遍历过的对象会被标记为黑色,表示它们是存活的对象。最后未被标记的对象会被回收。因为它们不再被存活的对象引用。
增量和并发
传统的垃圾收集算法会在垃圾收集的执行期间暂停应用程序,一旦触发垃圾收集,垃圾收集器会抢占 CPU 的使用权占据大量的计算资源以完成标记和清除工作,然而很多追求实时的应用程序无法接受长时间的 STW。
为了减少应用程序暂停的最长时间和垃圾收集的总暂停时间,我们会使用下面的策略优化现代的垃圾收集器:
- 增量垃圾收集 — 增量地标记和清除垃圾,降低应用程序暂停的最长时间;
- 并发垃圾收集 — 利用多核的计算资源,在用户程序执行时并发标记和清除垃圾;
因为增量和并发两种方式都可以与用户程序交替运行,所以我们需要使用屏障技术保证垃圾收集的正确性;
增量和并发的垃圾收集需要提前触发并在内存不足前完成整个循环,避免程序的长时间暂停。
增量收集器
增量收集器是一种垃圾回收器,它可以将垃圾回收的过程分成多个小步骤来执行,以减小对应用程序的停顿时间。
增量收集器在垃圾回收时,将整个垃圾回收过程分成多个阶段,每个阶段都会执行一小部分垃圾回收工作,然后让应用程序继续执行一段时间。垃圾回收器在每个小步骤之间,会留出一段时间让应用程序继续运行,从而减少了垃圾回收对应用程序的影响。
增量收集器可以大大减少垃圾回收的停顿时间,但同时也会增加垃圾回收的总时间,因为需要执行更多的小步骤。此外,增量收集器也可能会对应用程序的性能产生一定的影响,因为它需要在垃圾回收和应用程序之间切换执行。
许多现代的垃圾回收器都采用了增量收集器来平衡垃圾回收的停顿时间和总时间。
总结:增量式垃圾收集器是减少程序最长暂停时间的一种方案,就是将原本垃圾收集的过程分成一个个小步骤,在每个小不收执行之前会留出一段时间给应用程序运行,相当于将很长的STW划分成一个个很小的STW,这样可以减少很长的STW对应用程序的影响。但同时也增加了STW的总时间。
并发收集器
并发收集器利用多线程的优势可以和用户程序并发执行,并且可以缩短整个STW的时间。
但是并发的情况下可能会导致某些对象的状态发生改变,导致垃圾收集的正确性不太好,因此还需要加上读写屏障来保持垃圾收紧的正确性。
采用并发收集器增加垃圾收集的复杂性和性能开销。
屏障技术
屏障技术一般是防止CPU或者编译器对指令的重排序,保证指令执行的顺序性
垃圾收集的屏障技术可以分为两类:
写屏障:当程序中的一个对象引用被修改时,写屏障会记录这个修改,并向垃圾收集器发出信号,告诉它这个引用被修改,需要重新扫描
读屏障:当程序中的一个对象引用被读取时,读屏障会记录这个读取操作,并向垃圾收集器发出信号,告诉它这个引用已经被访问过,需要更新它的状态
写屏障和读屏障可以确保垃圾收集器可以准确的找到所有存活的对象,以免误判或者误删。
三色不变性
想要在并发或者增量的标记算法中保证正确性,我们需要达成以下两种三色不变性中的一种:
- 强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象
- 弱三色不变性:黑色对象指向的白色对象,必须包含一条从灰色对象经由多个白色对象的可达路径
遵循上述两种三色不变性中的一种,我们都能保证垃圾收集算法的正确性,屏障技术就是保证在增量或者并发过程中三色不变性的重要技术。
编程语言当中通常是采用写屏障来保证三色不变性。
读屏障会在读操作中增加代码片段,对用户性能影响大
插入写屏障
在一个垃圾收集器和用户程序交替的场景中
- 垃圾收集器会将根对象指向A对象标记为黑色,并将A对象指向的B对象标记为灰色
- 用户程序修改A对象的指针,将原本指向B对象的指针指向C对象,这时触发写屏障,将C对象标记为灰色
- 垃圾收集器依次遍历程序中的其他灰色对象,将它们标记为黑色
这种写屏障技术是一种相对保守的屏障技术,它会将有存活可能的对象都标记为灰色以满足强三色不变性。
但是它有明显的缺点,栈上的对象普遍认为是根对象,为了保证内存的安全与正确性。要在栈上添加写屏障或者在标记结束后再对栈上的对象进行扫描,前者会大幅度增加写入操作的开销,后者重新扫描栈对象时需要暂停线程。
删除写屏障
删除写屏障可以保证弱三色不变性
- 垃圾收集器将根对象A标记为黑色,并将A对象指向的B对象标记为灰色
- 用户程序将A对象原本指向B的指针指向C,触发删除写屏障,因为B对象已经是灰色的了,所以不做改变
- 用户程序将B对象原本指向C的对象指针删除,触发删除写屏障,白色的C对象被涂成灰色
- 垃圾收集器依次遍历其他灰色对象,将它们标记为黑色
删除写屏障通过对 C 对象的着色,保证了 C 对象和下游的 D 对象能够在这一次垃圾收集的循环中存活,避免发生悬挂指针以保证用户程序的正确性。
垃圾回收实现原理
目前比较常见的垃圾回收算法有3种:
- 引用计数:代表语言python
- 标记-清除:代表语言go
- 标记-复制 + 分代收集:代表语言java
go语言使用的是标记-清除、标记算法使用三色标记法在上面已经介绍过了
内存标记
span种一个个内存块用来分配内存,并由mspan结构中一个位图allowBits表示内存块的分配情况、gcmarkBits表示内存块的标记情况。在标记阶段就会用gcmarkBits对内存块进行标记。标记结束后,没有被gcmarkBits标记的内存块就会被回收。下一次标记会使用新的gcmarkBits
垃圾回收优化
golang的垃圾回收算法是标记-清除,是需要STW的,在处理器中就需要停止所有的goroutine,专心进行垃圾回收,待垃圾回收结束后再启动用户程序的goroutine。而STW直接影响了应用的执行,如果时间过长,是灾难性的。为了缩短STW,go语言每一个版本都在优化GC算法。其中写屏障和辅助GC就是两种优化的策略。
- 写屏障:在GC算法执行过程中,暂停用户程序是为了防止对象的状态发生改变,从而引起三色标记的混乱。而写屏障就是让用户程序和GC程序同时运行的手段,虽然有些地方还是需要STW的,但STW的时间也缩短了不少,写屏障能保证三色不变性。目前go使用了插入写屏障和删除写屏障,可能还有混合写屏障来保证三色不变性。
- 辅助GC:使用写屏障就可以让用户程序和GC程序并发的执行,但是如果用户程序分配内存的速度过快,也会导致内存不够。为了防止这种情况用户线程会辅助GC完成一部分的工作
垃圾回收触发机制
- 内存分配达到阈值:每次分配内存的时候看一下是否达到阈值,都有可能触发GC,阈值 = 上次GC分配量 * 内存增长率
- 定时触发GC:由go监控器启动定时,默认每两分钟执行一次GC
- 手动触发GC:可使用
runtime.GC()手动触发GC
参考博客: