大家好,我是木川
一、什么是 GC
垃圾回收也称为 GC(Garbage Collection),是一种自动内存管理机制
现代高级编程语言管理内存的方式分为两种:自动和手动,像C、C++ 等编程语言使用手动管理内存的方式,工程师编写代码过程中需要主动申请或者释放内存;而 PHP、Java 和 Go 等语言使用自动的内存管理系统,有内存分配器和垃圾收集器来代为分配和回收内存,其中垃圾收集器就是我们常说的GC。
在应用程序中会使用到两种内存,分别为堆(Heap)和栈(Stack),GC 负责回收堆内存,而不负责回收栈中的内存
栈是线程的专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈,函数执行完后,编译器可以将栈上分配的内存可以直接释放,不需要通过 GC 来回收。
堆是程序共享的内存,需要 GC 进行回收在堆上分配的内存。
垃圾回收器的执行过程被划分为两个半独立的组件:
- 赋值器(Mutator):这一名称本质上是在指代用户态的代码。因为对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作。
- 回收器(Collector):负责执行垃圾回收的代码。
二、主流 GC 算法
目前比较常见的垃圾回收算法有三种:
引用计数:为每个对象维护一个引用计数,当引用该对象的对象销毁时,引用计数 -1,当对象引用计数为 0 时回收该对象。
- 代表语言:Python、PHP、Swift
- 优点:对象回收快,不会出现内存耗尽或达到某个阈值时才回收。
- 缺点:不能很好的处理循环引用,而实时维护引用计数也是有损耗的。
分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。
- 代表语言:Java
- 优点:回收性能好
- 缺点:算法复杂
标记-清除:从根变量开始遍历所有引用的对象,标记引用的对象,没有被标记的进行回收。
- 代表语言:Golang(三色标记法)
- 优点:解决了引用计数的缺点。
- 缺点:需要 STW,暂时停掉程序运行。
三、Go GC 算法
GC 算法概念
Go 语言采用的是标记清除算法,并在此基础上使用了三色标记法和混合写屏障技术,G C过程和其他用户 goroutine 可并发运行,但需要一定时间的STW
三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。这里的三色,对应了垃圾回收过程中对象的三种状态:
- 白色:白色通常表示节点未被访问或未处理。在开始算法执行时,所有节点都被初始化为白色。
- 灰色:灰色表示节点已经被访问,但尚未完成对其子节点的访问。
- 黑色:黑色表示节点已经被访问并且已经完成了对其子节点的访问。
step 1: 创建:白、灰、黑 三个集合
step 2: 将所有对象放入白色集合中
step 3: 遍历所有root对象,把遍历到的对象从白色集合放入灰色集合 (这里放入灰色集合的都是根节点的对象)
step 4: 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,自身标记为黑色
step 5: 重复步骤4,直到灰色中无任何对象,其中用到2个机制:
- 写屏障(Write Barrier):上面说到的 STW 的目的是防止 GC 扫描时内存变化引起的混乱,而写屏障就是让 goroutine 与 GC 同时运行的手段,虽然不能完全消除 STW,但是可以大大减少 STW 的时间。写屏障在 GC 的特定时间开启,开启后指针传递时会把指针标记,即本轮不回收,下次 GC 时再确定。
- 辅助 GC(Mutator Assist):为了防止内存分配过快,在 GC 执行过程中,GC 过程中 mutator 线程会并发运行,而 mutator assist 机制会协助 GC 做一部分的工作。
step 6: 回收所有白色对象(垃圾)
root对象
根对象在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括:
全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上指向堆内存的指针。
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
插入写屏障
对象被引用时触发的机制(只在堆内存中生效):赋值器将插入行为通知给并发执行的回收器,进而产生可能需要额外(重新)扫描的对象 ,被引用的对象标记为灰色
缺点:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活
删除写屏障
对象被删除时触发的机制(只在堆内存中生效):赋值器将删除行为通知给并发执行的回收器,被删除的对象,如果自身为灰色或者白色,那么标记为灰色
缺点:一个对象的引用被删除后,即使没有其他存活的对象引用它,出于性能考虑,它仍然会活到下一轮(如果本轮回收会产生很大冗余扫描成本),降低了回收精度
混合写屏障
GC没有混合写屏障前,一直是插入写屏障;混合写屏障是插入写屏障 + 删除写屏障,写屏障只应用在堆上应用,栈上不启用(栈上启用成本很高)
- GC开始将栈上的对象全部扫描并标记为黑色。
- GC期间,任何在栈上创建的新对象,均为黑色。
- 被删除的对象标记为灰色。
- 被添加的对象标记为灰色。
GC 流程
一次完整的垃圾回收会分为四个阶段,分别是标记准备、标记开始、标记终止、清理:
- 标记准备(Mark Setup):打开写屏障(Write Barrier),需 STW(stop the world)
- 标记开始(Marking):使用三色标记法并发标记 ,与用户程序并发执行
- 标记终止(Mark Termination):对触发写屏障的对象进行重新扫描标记,关闭写屏障(Write Barrier),需 STW(stop the world)
- 清理(Sweeping):将需要回收的内存归还到堆中,将过多的内存归还给操作系统,与用户程序并发执行
GC 触发时机
主动触发:
- 调用 runtime.GC() 方法,触发 GC
被动触发:
- 定时触发,该触发条件由
runtime.forcegcperiod
变量控制,默认为 2 分 钟。当超过两分钟没有产生任何 GC 时,触发 GC - 根据内存分配阈值触发,该触发条件由环境变量GOGC控制,默认值为100(100%),当前堆内存占用是上次GC结束后占用内存的2倍时,触发GC
GC 算法演进
- Go 1:mark and sweep操作都需要STW
- Go 1.3:分离了mark和sweep操作,mark过程需要 STW,mark完成后让sweep任务和普通协程任务一样并行,停顿时间在约几百ms
- Go 1.5:引入三色并发标记法、插入写屏障,不需要每次都扫描整个内存空间,可以减少stop the world的时间,停顿时间在100ms以内
- Go 1.6:使用 bitmap 来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存,停顿时间在10ms以内
- Go 1.7:停顿时间控制在2ms以内
- Go 1.8:混合写屏障(插入写屏障和删除写屏障),停顿时间在0.5ms左右
- Go 1.9:彻底移除了栈的重扫描过程
- Go 1.12:整合了两个阶段的 Mark Termination
- Go 1.13:着手解决向操作系统归还内存的,提出了新的 Scavenger
- Go 1.14:替代了仅存活了一个版本的 scavenger,全新的页分配器,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的 STW 时间过长的问题