本文介绍垃圾回收算法。以 Golang 为例。
Golang 有三种触发 GC 的方式:
系统定时触发。若两分钟内没有触发 GC,则会自动触发一次。
系统显式触发。用户调用 runtime.GC 方法强制触发 GC。
申请内存时触发。申请堆空间时可能触发 GC。
垃圾回收算法分为两种:
引用计数。
标记清除算法。
标记清除算法
标记清除算法分为两个阶段:
标记阶段:从 GCRoot 对象开始,遍历所有直接引用和间接引用的对象,并将对象标记为“被引用”。
清除阶段:遍历堆中的所有对象,将所有未标记为“被引用”的对象删除。
删除对象并不是意味着回收内存,只是被删除对象的内存标记为可分配内存。 |
GCRoot 对象有多种来源,通常包括了:
栈上的局部变量。
全局变量和静态变量(位于 .bss/.data 段)
寄存器中的引用。
线程的栈帧。
Golang 在编译代码时会生成一些元信息,这些元信息可供 GC 是用以准确发现指针的位置。这些元信息包括了:
局部变量表:函数运行时会创建一个栈帧,这个栈帧中包括了方法的参数和局部变量。
清除阶段相对比较简单,因为内存分配器中记录了内存分配的详情。
标记清除算法逻辑比较简单,但是内存碎片比较严重。
三色标记算法
标记清除算法将对象标记为两种颜色,这导致 GC 过程中会导致 STW(Stop The World)。这是若对象和 GC 并发执行,则新创建的对象在第一个阶段未被标记为可访问,从而在第二个阶段导致内存被错误地回收,因此 GC 过程中需要 STW 以避免新的内存分配。要解决这个问题,就需要是用三色标记算法。
三色标记算法将对象分为黑白灰三种对象,GC 过程如下:
所有对象初始时都是白色。
GC 第一次扫描将 GCRoots 全部标记为灰色。
将灰色集合中所有没有子引用的对象标记为黑色,并将所有子引用对象标记为灰色。
重复上述步骤,直至所有对象都被标记为黑色。
遍历堆对象,删除所有的白色对象。
标记过程和用户代码是并发执行的,这就可能出现两种情况:
被标记为黑色对象的引用链被断开转变为白色,导致此次 GC 没有删除。这种情况下此对象不进行额外处理,对象的删除被推迟到下一次。
与灰色对象相连的白色对象断开了引用,又与黑色对象建立了引用,从而导致不可能被标记为灰色,对象被错误地理解为白色。
针对第二种情况有两种解决办法:
若黑色对象被插入了新的引用,则黑色对象会被记录下来,标记结束后再扫描一次。
若灰色对象解除了引用,则将被解除的白色引用记录下来,标记结束后以白色引用为根再进行一次扫描。
分代回收算法
分代回收算法是是内存管理方式的一种,和上面的标记清除算法并不冲突。分代回收算法将对象根据生命周期进行划分,从而减少内存碎片。分代回收将内存分为下面区域:
内存管理流程如下:
新创建的对象被分配到 Eden 中。
Eden 满,触发一次垃圾回收。将 Eden 中的对象对象移到幸存区 To 中。然后交换 From 和 To。
对幸存区 To 进行一次垃圾回收。
每次垃圾回收都会导致对象的寿命加一,当寿命超过阈值后就会将对象移到老年代中。
若老年代也满了,则触发一次 Full GC。