GC实现原理
什么是GC?
垃圾回收也称为GC(Garbage Collection),是一种自动内存管理机制
现代高级编程语言管理内存的方式分为两种:自动和手动,像C、C++ 等编程语言使用手动管理内存的方式,工程师编写代码过程中需要主动申请或者释放内存;而 PHP、Java 和 Go 等语言使用自动的内存管理系统,有内存分配器和垃圾收集器来代为分配和回收内存,其中垃圾收集器就是我们常说的GC。
在应用程序中会使用到两种内存,分别为堆(Heap)和栈(Stack),GC负责回收堆内存,而不负责回收栈中的内存:
栈是线程的专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈,函数执行完后,编译器可以将栈上分配的内存可以直接释放,不需要通过GC来回收。
堆是程序共享的内存,需要GC进行回收在堆上分配的内存。
垃圾回收器的执行过程被划分为两个半独立的组件:
- 赋值器(Mutator):这一名称本质上是在指代用户态的代码。因为对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作。
- 回收器(Collector):负责执行垃圾回收的代码。
主流的GC算法
目前比较常见的垃圾回收算法有3种:
- 引用计数:为每个对象维护一个引用计数,当引用该对象的对象销毁时,引用计数-1,当对象引用计数为0时回收该对象。
- 代表语言:Python、PHP、Swift
- 优点:对象回收快,不会出现内存耗尽或达到某个阈值时才回收。
- 缺点:不能很好的处理循环引用,而实时维护引用计数也是有损耗的。
- 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。
- 代表语言:Java
- 优点:回收性能好
- 缺点:算法复杂
- 标记-清除:从根变量开始遍历所有引用的对象,标记引用的对象,没有被标记的进行回收。
- 代表语言:Golang(三色标记法)
- 优点:解决了引用计数的缺点。
- 缺点:需要 STW,暂时停掉程序运行。
Go GC算法
此算法是在Go1.5版本开始使用,Go采用的是标记清除算法,并在此基础上使用了三色标记法和混合写屏障技术,GC过程和其他用户goroutine可并发运行,但需要一定时间的STW
三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。这里的三色,对应了垃圾回收过程中对象的三种状态:
- 灰色:对象还在标记队列中等待
- 黑色:对象已被标记,
gcmarkBits
对应位为1(该对象不会在本次GC中被回收) - 白色:对象未被标记,
gcmarkBits
对应位为0(该对象会在本次GC中被清理)
垃圾回收过程:
- 创建:白,灰,黑三个集合
- 将所有对象放入白色集合中
- 遍历所有root对象,把遍历到的对象从白色集合放入灰色集合(这里放入灰色集合的都是根节点的对象)
- 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,自身标记为黑色
- 重复步骤4,直到灰色中无任何对象,其中用到2个机制:
- 写屏障(Write Barrier):上面说到的STW目的是防止GC扫描时内存变化引起的混乱,而写屏障就是让
Goroutine
与GC同时运行的手段,虽然不能完全消除STW,但是可以大大减少STW时间。写屏障在GC的特定时间开启,开启后指针传递时会把指针标记,即本轮不回收,下次GC时再确定 - 辅助 GC(Mutator Assist):为了防止内存分配过快,在 GC 执行过程中,GC 过程中 mutator 线程会并发运行,而 mutator assist 机制会协助 GC 做一部分的工作。
- 写屏障(Write Barrier):上面说到的STW目的是防止GC扫描时内存变化引起的混乱,而写屏障就是让
- 收集所有白色对象(垃圾)
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如何调优
- 控制内存分配的速度,限制
Goroutine
的数量,提高赋值器mutator
的CPU利用率(降低GC的CPU利用率) - 少量使用
+
连接string
slice
提前分配足够的内存来降低扩容带来的拷贝- 避免
map key
对象过多,导致扫描时间增加 - 变量复用,减少对象分配,例如使用
sync.Pool
来复用需要频繁创建临时对象,使用全局变量 - 增大
GOGC
的值,降低GC的运行频率