24 垃圾回收

[TOC]

垃圾回收

垃圾回收(Garbage Collection,GC)是自动内存管理的一种形式,通常由GC收集并适时回收或重用不再被对象占用的内存。

  • GC是内存管理的一部分,包含重要的功能:分配和管理新对象、识别正在使用的对象、清除不再使用的对象。

  • 垃圾回收可以减少错误和复杂性,需要手动分配、释放内存的语言容易发生内存泄露和野指针问题,GC虽然不能完全避免内存泄露,但是它能保证对象最终会被收集,避免了悬空指针、多次释放等手动管理内存时会出现的问题。具有GC的语言屏蔽了内存管理的复杂性,开发者可以更好地关注核心的业务逻辑。

  • 垃圾回收会带来额外的成本,需要保存内存的状态信息、扫描内存,大部分情况下要中断整个程序来处理垃圾回收,因此对于嵌入式、系统级程序等程序速度要求高、内存要求小的场景不适用,但是开发大规模、分布式、微服务集群等是适用的。

5种垃圾回收经典算法

标记-清扫(三色标记清扫)

标记清扫(Mark-Sweep)算法是比较早的垃圾回收算法。

  • 标记清扫算法分两个阶段,第1阶段是扫描并标记当前活着的对象,第2阶段是清扫没有被标记的对象。

  • 标记清扫是一种间接的垃圾回收算法,它不直接查找垃圾对象,而是通过活着的对象推断出垃圾对象。

  • 扫描从栈的根对象开始,只要对象引用了其它堆对象就一直向下扫描,因此可以采取深度优先搜索或广度优先搜索的方式进行扫描。

  • 在扫描阶段,通过颜色对对象的状态进行抽象有效管理扫描对象的状态,如将已经扫描的对象标记为黑色、没被扫描的标记为灰色、没被扫描可能是垃圾对象的标记为白色。

  • 缺点:标记清扫算法的主要缺点在于可能产生内存碎片,会导致新对象分配失败,如中间区域内存已经被分配,留下两端一些散碎的内存空间,将很难被利用。。

标记压缩

标记压缩(Mark-Compact)算法通过将分散的、存活的对象移动到更紧密的空间来解决内存碎片问题。

  • 标记压缩算法分为标记与压缩两个阶段。

  • 标记压缩的标记过程和标记清扫算法的标记过程类似。

  • 在压缩阶段先扫描存活的对象并将其压缩到空闲的区域,以此保证压缩后的空间更紧凑从而解决内存碎片问题。

  • 压缩后的空间查找空闲的内存区域速度更快。

  • 缺点:标记压缩算法的缺点在于对象在内存的位置是随机的,这会破坏缓存的局部性,并且时常需要一些额外的空间来标记当前对象已经移动到了其它地方。在压缩时B对象发生了转移,那就需要更新所有引用了B对象的指针,这增加了实现的复杂性。

半空间复制

半空间赋值(Semispace Copy)是用空间换时间的算法

  • 半空间复制不分阶段,扫描根对象时直接进行压缩,每个扫描到的对象都会从fromspace空间复制到tospace空间。

  • 一次使用一半空间,空闲另一半空间,扫描复制存活的对象到另一半然后回收这一半,如此往返,这样消除了内存碎片问题。

  • 缺点:只使用一半空间,对内存的消耗很大。

引用计数

引用计数(Reference Counting)

  • 每个对象都包含一个引用计数,每当其它对象引用了此对象时,引用计数就会增加,取消引用后引用计数就会减少。

  • 引用计数为0就表明该对象为垃圾对象。

  • 引用计数算法简单高效,在垃圾回收阶段不需要额外占用大量内存,即便垃圾回收系统的一部分出现异常,也能有一部分对象被正常回收。

  • 缺点:只是只读操作或者循环迭代操作也会更新引用计数,频繁更新引用计数,并且引用计数更新还必须是原子性操作,并发操作同一个对象会导致引用计数难以处理自引用的对象。

分代GC

分代GC是将对象按照存活时间进行划分

  • 分代GC的重要前提是:死去的对象一般都是新创建不久的,因此没必要反复扫描旧对象,这大概率会加快垃圾回收的速度,提高处理能力和吞吐量,减少程序暂停的时间。

  • 缺点:分代GC没有办法回收老一代对象,并且需要额外开销引用和区分新老对象,特别是在有多代对象的时候。

Go的垃圾回收

Go采用并发三色标记算法进行垃圾回收。

  • 引用计数由于其固有的缺陷在并发时很少使用,不适合高并发语言。

  • 压缩算法的优势是减少碎片并能快速分配内存,Go使用了TCmalloc内存分配算法,虽然没有压缩算法那样极致,但它已经很好解决了内存碎片的问题。

  • 分代GC主要假设大部分变成垃圾的对象都是新创建的,但是Go编译器进行了优化,通过内存逃逸的机制将会继续使用的对象转移到了堆中,大部分生命周期短的对象会在栈中分配,这样减弱了分代GC的优势。另外分代GC需要额外的写屏障保护并发垃圾回收时对象的隔代性,会减慢GC的速度。

并发三色标记垃圾回收

Go的垃圾回收算法是并发三色标记,Go的垃圾回收算法在各版本不断改进提高性能。

  • Go 1.0 单协程垃圾回收,在垃圾回收开始阶段需要停止所有用户协程,并且在垃圾回收阶段只有一个协程执行垃圾回收。

  • Go 1.1 多个协程进行垃圾回收,加快了垃圾回收的速度,但还是会停止所有用户协程。

  • Go 1.5 对垃圾回收进行了重大更新,允许用户协程在后台的垃圾回收同时执行,降低了用户协程暂停的时间。

  • Go 1.6 减少了STW(Stop The World)期间的任务,降低了用户协程暂停的时间。

  • Go 1.8 使用了混合写屏障技术消除了栈重新扫描的时间,将用户协程暂停的时间进一步降低。

三色抽象和不变性

原始的标记清理是一个串行的过程,这种方法能够简化回收器的实现,因为只需要让回收器开始执行时, 将并发执行的赋值器挂起。这种情况下,对用户代码而言,回收器是一个原子操作。 如果标记过程并发执行,当赋值器在执行时同时执行回收器就面临程序的正确性问题,必须保障回收器不会将存活的对象进行回收,回收器也必须保证赋值器能够正确访问到已经被重新整理和移动的对象。

  • 三色抽象是一种描述追踪式回收器的方法,在实践中并没有实际含义,它的重要作用在于从逻辑上严密推导标记清理这种垃圾回收方法的正确性。

从垃圾回收器的视角来看,三色抽象规定了三种不同类型的对象,并用不同的颜色相称

  • 白色对象(可能死亡):未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均为死亡的不可达对象。

  • 灰色对象(波面):已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。

  • 黑色对象(确定存活):已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。

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

  • 垃圾回收开始时,只有白色对象。随着标记开始,灰色对象出现,波面扩大(扫描范围扩大)。

  • 当一个对象的所有子节点均完成扫描时,会被着色为黑色。当整个堆遍历完成时,只剩下黑色和白色对象,这时的黑色对象为存活的可达对象;

  • 白色对象为死亡的不可达对象,这个过程可以视为以灰色对象为扫描的起始位置,将黑色对象和白色对象分离,使波面不断向前推进,直到所有可达的灰色对象都变为黑色对象为止的过程。

垃圾回收器波面抽象

垃圾回收器波面抽象

垃圾回收的流程

Go的并发三色标记的垃圾回收策略从根对象(协程栈、全局对象)出发查找所有被引用的对象。

  • Go在尽可能不影响用户协程的情况下引入辅助标记、辅助清扫、系统驻留内存清扫、混合写屏障等策略保证快速执行垃圾回收。

  • 默认情况下占用内存达到上次GC标记内存的2倍后触发垃圾回收,在并发阶段消耗25%的CPU执行后台标记协程。

  • 清扫阶段采取了懒清扫策略,并且有专门的后台协程耗费1% CPU的时间执行系统驻留内存清除。

垃圾回收循环

当内存到达垃圾回收的阈值后,会触发新一轮的垃圾回收。

  • 会先后经历标记准备阶段、并行标记阶段、标记终止阶段、垃圾清扫阶段。

  • 在并行标记阶段引入了辅助标记技术,在垃圾清扫阶段还引入了辅助清扫、系统驻留内存清除技术。

垃圾回收循环

垃圾回收循环

标记准备阶段

标记准备阶段执行轻量级任务,主要是清扫上一阶段GC遗留的需要清扫的对象,因为使用了懒清扫算法,所以当执行下一次GC时可能还存在没有被清扫的垃圾对象。

  • 标记准备阶段会重置各种状态和统计指标、启动专门用于标记的协程、统计需要扫描的任务数量、开启写屏障、启动标记协程等。

  • 标记准备阶段会为每个逻辑处理器P启动一个标记协程,但并不是所有的标记协程都有执行的机会,因为在标记阶段标记协程与用户协程是并行的(这是为了减少GC对程序造成的影响)。

  • 标记准备阶段会计算当前需要开启多少标记协程,Go规定标记协程消耗的CPU接近25%。

并发标记阶段

并发标记阶段后台标记协程与用户协程并行。

  • Go的后台标记协程占用CPU的时间为25%,以最大限度避免执行GC而中断或减慢用户协程的执行。

  • 这个阶段的主要目的是扫描对象进行标记。

  • 标记任务3种不同的模式:

标记任务3种不同的模式

标记任务3种不同的模式

  • 后台4种flag标记协程的不同行为:

后台4种flag标记协程的不同行为

后台4种flag标记协程的不同行为

DedicatedMode模式下,会一直执行后台标记任务,意味着逻辑处理器P本地队列中的协程会一直得不到执行。

  • 所以Go的做法是先执行可以被抢占的后台标记任务,如果标记线程已经被其它协程抢占,那当前的逻辑处理器P并不会执行其它协程,而是将其它协程转移到全局队列中,并取消gcDrainUntilPreempt标志,进入不能被抢占的模式。

FractionalMode 模式和IdleMode 模式都允许被抢占。

  • FractionalMode 模式加上了gcDrainFractional 标记表明当前标记协程会在到达目标时间后退出,IdleMode 模式加上了gcDrainIdle标记表明在发现有其它协程可以运行时退出当前标记协程。

三种模式都加上了gcDrainFlushBgCredit标志,用于计算后台完成的标记任务量,并唤醒之前由于分配内存太频繁而陷入等待的用户协程。

扫描

根对象扫描

  • 在最开始的标记准备阶段会统计这次GC一共要扫描多少对象。

  • 从根对象出发,可以找到所有的引用对象(存活的对象)。

  • Go中,根对象包括全局变量、span中finalizer的任务数量、所有协程栈。

  • finalizer 是Go中对象绑定的析构器,当对象内存释放后,需要调用析构器函数完成释放资源。

全局变量扫描

  • 在运行时确定全局变量被分配到虚拟内存的哪一个区域,如果全局变量有指针,那么在运行时其指针指向的内存可能变化。

  • 在编译时,可以确定全局变量中哪些位置包含指针。Go对内存的精细化管理可以通过指针找到指针对应的对象位置。

栈扫描

  • 栈扫描是根对象扫描中最重要的部分。

  • 运行时计算出当前协程的所有栈帧信息。

  • 编译时能够得知栈上有哪些指针,以及对象中的哪一部分包含了指针。

  • 运行时首先计算出栈帧布局,每个栈帧都代表一个函数,运行时可以得知当前栈帧的函数参数、函数本地变量、寄存器SP、BP等一系列信息。

  • 每个栈帧函数的参数和局部变量,都需要进行扫描,确认该对象是否任然在使用,如果在使用则需要扫描位图判断对象中是否包含指针。

栈对象

  • 栈扫描对部分被重复赋值覆盖的对象无法确定是否应该被释放,因此使用栈对象弥补。

  • 栈对象是在栈上能够被寻址的对象,存储在寄存器中的变量就是不能被寻址的。

  • 编译器在编译时将所有的栈对象都记录下来,同时,编译器将追踪栈中所有可能指向栈对象的指针。在垃圾回收期间,所有栈对象都会存储到一棵二叉树中。

扫描灰色对象

  • 进行根对象扫描时,会将标记的对象放入本地队列中,如果本地队列放不下就放入全局队列中,这种设计是为了最大限度避免使用锁,在本地缓存的队列可以被逻辑处理器P无锁访问。

  • 在标记期间会循环往复地标记队列获取灰色对象,灰色对象扫描到的白色对象仍然会被放入标记队列中,如果扫描到已经被标记的对象则忽略,一直到队列中的任务为空为止。

finalizer

finalizer是特殊对象,它是对象释放后会被调用的析构器,用于资源释放。析构器不会被栈上或全局变量引用,需要单独处理。

  • finalizer 可以将资源的释放托管给垃圾回收,这个功能在CGO中经常使用。在Go调用C函数时,C函数分配的内存不受Go垃圾回收的管理,这时可以借助defer函数调用结束时释放内存。使用runtime.SetFinalizer()函数托管C创建的资源的指针。

标记终止阶段

完成并发标记阶段所有灰色对象的扫描和标记后进入标记终止阶段,标记终止阶段主要完成统计用时、统计强制开始GC的次数、更新下一次触发GC需要达到的堆目标、关闭写屏障、唤醒后台清扫的协程,开始下一阶段的清扫工作。

  • 标记终止阶段的重要任务是计算下一次触发GC时要达到的堆目标,这是垃圾回收的调步算法。

  • 在GC开始到结束的过程中,用户协程可能被分配了大量的内存,所以在GC的过程中,程序占用的内存的大小实际上超过了设定的触发GC的目标。为了解决这样的问题需要对程序进行估计,从而在达到内存占用量目标之前就启动GC,并保证GC结束之后,占用内存的大小刚好在目标内存附近。

辅助标记

辅助标记算法是为了解决并发标记阶段扫描内存的同时用户协程也在不断被分配内存,当用户协程的内存分配速度快到标记协程来不及扫描时,GC标记阶段将永远不会结束,从而无法完成完整的GC周期造成内存泄露。

  • 辅助标记必须在垃圾回收的标记阶段进行,用户协程被分配了超过限度的内存时其暂停并切换到辅助标记工作。

  • 在GC并发标记阶段,当用户协程分配内存时,会先检查是否已经完成了指定的扫描工作。

赋值器

赋值器的颜色

  • 回收器没有停止所有赋值器,并发的用户态代码下就涉及了存在的多个不同状态的赋值器。

  • 把回收器视为对象,把赋值器视为影响回收器这一对象的实际行为(即影响 GC 周期的长短), 从而引入赋值器的颜色。

  • 黑色赋值器:已经由回收器扫描过,不会再次对其进行扫描。

  • 灰色赋值器:尚未被回收器扫描过,或尽管已经扫描过但仍需要重新扫描。

赋值器的颜色对回收周期的结束产生的影响:

  • 如果某种并发回收器允许灰色赋值器的存在,则必须在回收结束之前重新扫描对象图。

  • 如果重新扫描过程中发现了新的灰色或白色对象,回收器还需要对新发现的对象进行追踪, 但是在新追踪的过程中,赋值器仍然可能在其根中插入新的非黑色的引用,如此往复, 直到重新扫描过程中没有发现新的白色或灰色对象。

  • 于是,在允许灰色赋值器存在的算法,最坏的情况下, 回收器只能将所有赋值器线程停止才能完成其根对象的完整扫描,也就是STW。

新分配对象的颜色

  • 新的分配过程会导致赋值器持有新分配对象的引用。要为新产生的对象分配适当的颜色。

  • 如果新分配的对象为黑色或者灰色,则赋值器直接将其视为无需回收的对象,写入堆中。

  • 如果新分配的对象为白色,则可以避免无意义的新对象保留到下一个垃圾回收的周期。

  • 由于黑色赋值器已经被回收器扫描过,不会再对其进行任何扫描,一旦其分配新的白色对象 则意味着会导致错误的回收。因此黑色赋值器不能产生白色对象,除非赋值器能够保证分配的白色对象的引用被写入到灰色波面中,但这实践起来并不容易。为了简化实现复杂度,令新分配的对象为黑色通常是安全的。

弱三色不变性 垃圾回收器的正确性体现在:不应出现对象的丢失,也不应错误的回收还不该被回收的对象。 作为内存屏障的一种,写屏障(Write Barrier)是一个在并发垃圾回收器中才会出现的概念。

  • 可以证明,当以下两个条件同时满足时会破坏垃圾回收器的正确性 [Wilson, 1992]:

  • 条件 1: 赋值器修改对象图,导致某一黑色对象引用白色对象。

  • 条件 2: 从灰色对象出发,到达白色对象的未经访问过的路径被赋值器破坏。

  • 只要能够避免其中任何一个条件,则不会出现对象丢失的情况,因为:

  • 如果条件 1 被避免,则所有白色对象均被灰色对象引用,没有白色对象会被遗漏。

  • 如果条件 2 被避免,即便白色对象的指针被写入到黑色对象中,但从灰色对象出发,总存在一条没有访问过的路径,从而找到到达白色对象的路径,白色对象最终不会被遗漏。

  • 我们将三色不变性所定义的波面根据这两个条件进行削弱:

  • 当满足原有的三色不变性定义(或上面的两个条件都不满足时)的情况称为强三色不变性(strong tricolor invariant)

  • 当赋值器令黑色对象引用白色对象时(满足条件 1 时)的情况称为弱三色不变性(weak tricolor invariant)

  • 当赋值器进一步破坏灰色对象到达白色对象的路径时(进一步满足条件 2 时),即打破弱三色不变性, 也就破坏了回收器的正确性;或者说,在破坏强弱三色不变性时必须引入额外的辅助操作。 弱三色不变性的好处在于:只要存在未访问的能够到达白色对象的路径,就可以将黑色对象指向白色对象。

屏障技术 (内存屏障 Memory Barrier)

屏障技术是为了保障代码中对内存的操作顺序 既不会在编译期被编译器进行调整,也不会在运行时被 CPU 的乱序执行所打乱,是一种语言与语言用户间的契约。

  • 辅助标记解决的是垃圾回收正常结束与循环的问题,在并发标记时会造成对象颜色的准确性问题,屏障技术将解决准确性问题。

  • 保证并发标记准确性需要遵守的原则是强、弱三色不变性,强三色不变性指的是所有白色对象都不能被黑色对象引用,这是一种比较严格的要求。与之对应的是弱三色不变性,弱三色不变性允许白色对象被黑色对象引用,但是白色对象必须有一条路径始终是被灰色对象引用的,这保证了该对象最终能被扫描到。

  • 在并发标记写入和删除对象时,可能破坏三色不变性,因此必须有一种机制能够维护三色不变性,这就是屏障技术。屏障技术的原则是在写入或者删除对象时将可能活着的对象标记为灰色。

  • 屏障在写入和删除时会重新标记颜色,以此保证三色不变性,解决并发标记的准确性问题。

阶段
说明
赋值器状态

清扫终止

为下一个阶段的并发标记做准备工作,启动写屏障

STW

标记

与赋值器并发执行,写屏障处于开启状态

并发

标记终止

保证一个周期内标记任务完成,停止写屏障

STW

内存清扫

将需要回收的内存归还到堆中,写屏障处于关闭状态

并发

内存归还

将过多的内存归还给操作系统,写屏障处于关闭状态

并发

写屏障

  • 垃圾回收器的写屏障是指赋值器的写屏障。

  • 赋值器写屏障作为一种同步机制,使赋值器在进行指针写操作时,能够通知回收器,进而不会破坏弱三色不变性。

  • 屏障上需要依赖多种操作来应对指针的插入和删除

  • 扩大波面:将白色对象作色成灰色

  • 推进波面:扫描对象并将其着色为黑色

  • 后退波面:将黑色对象回退到灰色

  • 根据灰色赋值器和黑色赋值器的不同,分别会有不同类别的赋值器屏障。与 Go 有关的两个赋值器屏障: 灰色赋值器的 Dijkstra 插入屏障 与 黑色赋值器的 Yuasa 删除屏障。

插入屏障 Dijkstra

  • 灰色赋值器的 Dijkstra 插入屏障

  • 插入屏障(insertion barrier)技术,又称为增量更新屏障(incremental update) 。 它的核心思想是把赋值器对已存活的对象集合的插入行为通知给回收器,进而产生可能需要额外(重新)扫描的对象。

  • 如果某一对象的引用被插入到已经被标记为黑色的对象中,这类屏障会保守地将其作为非白色存活对象, 以满足强三色不变性。

  • Dijkstra 插入屏障作为诸多插入屏障中的一种,对于插入到黑色对象中的白色指针,无论其在未来是否会被赋值器删除,该屏障都会将其标记为可达(着色)。

  • Dijkstra 插入屏障有性能上的优势,它不需要对指针进行任何处理,因为指针的读操作通常比写操作高出一个或更多数量级。

  • 但是由于 Dijkstra 插入屏障的保守,在一次回收过程中可能会产生一部分被染黑的垃圾对象,只有在下一个回收过程中才会被回收;

  • 在标记阶段中,每次进行指针赋值操作时,都需要引入写屏障,这会增加性能开销,为了避免造成性能问题,可以选择关闭栈上的指针写操作的 Dijkstra 屏障。当发生栈上的写操作时,将栈标记为恒灰(permagrey),但此举产生了灰色赋值器,需要标记终止阶段 STW 时对这些栈进行重新扫描。

删除屏障 Yuasa

  • 黑色赋值器的 Yuasa 删除屏障

  • 删除屏障(deletion barrier)技术,又称为基于起始快照的屏障(snapshot-at-the-beginning)。

  • 当赋值器从灰色或白色对象中删除白色指针时,通过写屏障将这一行为通知给并发执行的回收器。 这一过程很像是在操纵对象图之前对图进行了一次快照。

  • 如果一个指针位于波面之前,则删除屏障会保守地将目标对象标记为非白色存活对象,进而避免[从灰色对象到达白色对象的路径被赋值器破坏]来满足弱三色不变性。 具体来说,Yuasa 删除屏障在回收过程中,对于被赋值器删除最后一个指向这个对象导致该对象不可达的情况,仍将其对象进行着色。

  • Yuasa 删除屏障的优势在于不需要标记结束阶段的重新扫描,结束时候能够准确的回收所有需要回收的白色对象。 缺陷是 Yuasa 删除屏障会拦截写操作,进而导致波面的退后,产生冗余的扫描,

混合写屏障

  • Go 在 1.8 的时候为了简化 GC 的流程,同时减少标记终止阶段的重扫成本, 将 Dijkstra 插入屏障和 Yuasa 删除屏障进行混合,形成混合写屏障。

  • 混合写屏障会对正在被覆盖的对象进行着色,且如果当前栈未扫描完成, 则同样对指针进行着色。

  • 在 Go 1.8 之前,为了减少写屏障的成本,Go 选择没有启用栈上写操作的写屏障, 赋值器总是可以通过将一个单一的指针移动到某个已经被扫描后的栈, 从而导致某个白色对象被标记为灰色进而隐藏到黑色对象之下,就需要对栈的重新扫描, 甚至导致栈总是灰色的,因此需要 STW。

  • 混合写屏障是为了消除栈的重扫过程,因为一旦栈被扫描变为黑色,则它会继续保持黑色, 并要求将对象分配为黑色。

  • 在 Go 1.8 的实现中,如果无条件对引用双方进行着色,自然结合了 Dijkstra 插入屏障 和 Yuasa 删除屏障的优势, 但是因为着色成本是双倍的,编译器需要插入的代码也成倍增加, 随之带来的结果就是编译后的二进制文件大小也进一步变大。为了针对写屏障的性能进行优化, Go 1.10 和 Go 1.11 中,Go 实现了批量写屏障机制。 其基本想法是将需要着色的指针统一写入一个缓存, 每当缓存满时统一对缓存中的所有指针进行着色。

浮动垃圾

  • 插入屏障和删除屏障通过写入和删除时重新标记颜色保证了三色不变性,解决了并发标记期间的准确性问题,但是它们都存在浮动垃圾的问题。

  • 插入屏障在删除引用时,可能标记一个已经变成垃圾的对象,删除屏障在删除引用时可能把一个垃圾对象标记为灰色。这是垃圾回收的精度问题,不会影响它的准确性,因为浮动垃圾会在下一次垃圾回收中被回收。

垃圾清扫

垃圾标记工作完成意味已经追踪到内存中所有存活的对象,之后进入垃圾清扫阶段,将垃圾对象的内存回收重用或返还给操作系统。

  • 清扫协程被唤醒后会开始垃圾清扫。垃圾清扫采取了懒清扫的策略,执行少量清扫工作后,通过Gosched函数让渡执行权,不需要一直执行,因此当触发下一阶段的垃圾回收后,可能存在之前没有清理的内存,需要先将它们清理。

懒清扫逻辑

  • 以span为单位进行清扫。

  • 先从mheap中的sweepSpans队列中取出需要清扫的span,清扫完成后span会被mheap回收,并更新整个基数树表明当前span的整个空间都可以被程序再次使用。

  • 这种清扫方式并没有直接将内存释放到操作系统中,而是再次组织内存以便能在下次内存分配时利用已经被回收的内存。

辅助清扫

  • 由于是通过懒清扫的形式进行垃圾清扫,在下一次触发GC时必须将上一次GC未清扫的span全部扫描。

  • 如果剩余的未清扫的span太多,会拖延下一次GC开始的时间,为了规避这个问题,Go使用了辅助清扫的手段。

  • 用户协程会在适当的时机执行辅助清扫工作,以避免下一次GC发生时还有大量未清扫的span。判断是否需要辅助清扫的最好时机是在用户协程分配内存时。

  • 在需要向mcentrel申请内存时或在为大对象分配内存时会判断是否需要辅助扫描,已经清扫的page数不大于清理的目标page数就会进行辅助清扫。

系统驻留内存清除

  • 驻留内存 RSS 是主内存 RAM 保留的进程占用的内存部分,是从操作系统中分配的内存。

  • 为了将系统分配的内存保持在适当的大小,同时回收不再被使用的内存,Go使用了单独的后台清扫协程来清除内存。后台清扫协程在程序开始时只启动一个。

  • 清除策略占用当前线程CPU 1% 的时间进行清除,所以在大部分时间里后台清扫协程都在休眠。

Last updated