垃圾回收就是对程序中不再使用的内存资源进行自动回收的操作。
标记-清除 算法
分为两个阶段,标记(Mark)阶段 和 清除(Sweep)两个阶段。
- Mark阶段:从根对象开始,对内存对象进行遍历,对所有可达的对象进行标记。
- Sweep阶段:对Mark阶段未被标记的内存对象进行回收,回收完毕后重置所有的内存对象的标记以便下轮“标记-清除”。
什么是根对象?
指应用程序中可以通过变量直接访问的对象。
如图所示,所有对象都可以通过跟对象访问到,所以就没内存对象需要回收。
如果应用程序之后将对象C成员对H的引用去除:
这种情况,在标记阶段,就不会对 H 进行标记
那在清除阶段,H对象就会被回收。
三色标记法
三色标记算法将程序中的对象分成白色、黑色和灰色
- 白色 - 潜在垃圾:表示还未搜索到的对象,其内存可能会被垃圾收集器回收;
- 灰色 - 活跃对象:表示正在搜索还未搜索完的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
- 黑色 - 活跃对象:表示搜索完成的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象;
实现过程:白 → 灰 → 黑
详细点的实现过程
- 初始化:所有对象初始状态为白色。
- 初始标记阶段:从根对象开始,标记所有直接可达的对象为灰色,并将它们放入待扫描队列中。
- 并发标记阶段:并发地扫描待扫描队列中的对象,当发现一个灰色对象时,将其标记为黑色,并将其子对象标记为灰色,并放入待扫描队列中。直到待扫描队列为空。
- 重新标记阶段:停止程序执行,重新扫描一遍所有对象,标记出所有的灰色对象。这个阶段可能会造成一定的停顿时间。
- 清理阶段:清理所有的白色对象,并将它们的内存释放回空闲列表,以便后续分配新对象时使用。这个阶段的工作也是并发进行的,不会对程序的执行造成停顿。
STW是什么?
STW是指垃圾回收器在执行垃圾回收时暂停整个应用程序的运行。在STW期间,应用程序的所有线程都会被停止,直到垃圾回收完成。
写屏障
写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。
可以简单的理解为:在每一轮GC开始时会初始化一个叫做“屏障”的东西,然后由它记录第一次scan时各个对象的状态,以便和第二次re-scan进行比对,引用状态变化的对象被标记为灰色以防止丢失,将屏障前后状态未变化对象继续处理。
GC如何工作
- Mark:包含两部分
- Mark Prepare:初始化GC任务,包括开启写屏障(write barrier)和辅助GC(mutator assist),统计root对象的任务数量等。这个过程需要STW
- GC Drains: 扫描所有root对象,包括全局指针和goroutine(G)栈上的指针(扫描对应G栈时需停止该G),将其加入标记队列(灰色队列),并循环处理灰色队列的对象,直到灰色队列为空。该过程后台并行执行
- Mark Termination:完成标记工作,重新扫描(re-scan)全局指针和栈。因为Mark和用户程序是并行的,所以在Mark过程中可能会有新的对象分配和指针赋值,这个时候就需要通过写屏障(write barrier)记录下来,re-scan 再检查一下。这个过程也是会STW的。
- Sweep: 按照标记结果回收所有的白色对象,该过程后台并行执行
- Sweep Termination: 对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC。
辅助GC
从上面的GC工作的完整流程可以看出Golang GC实际上把单次暂停时间分散掉了,本来程序执⾏可能是“⽤户代码–>⼤段GC–>⽤户代码”,那么分散以后实际上变成了“⽤户代码–>⼩段 GC–>⽤户代码–>⼩段GC–>⽤户代码”这样。如果GC回收的速度跟不上用户代码分配对象的速度呢? Go 语⾔如果发现扫描后回收的速度跟不上分配的速度它依然会把⽤户逻辑暂停,⽤户逻辑暂停了以后也就意味着不会有新的对象出现,同时会把⽤户线程抢过来加⼊到垃圾回收⾥⾯加快垃圾回收的速度。这样⼀来原来的并发还是变成了STW,还是得把⽤户线程暂停掉,要不然扫描和回收没完没了了停不下来,因为新分配对象⽐回收快,所以这种东⻄叫做辅助回收。
GC触发条件