近期在改 Lua 5.4 的垃圾回收,虽然之前也写过分代垃圾回收的原理,但这次改完之后对其更有感悟,就简单记录一下Lua 5.4 的分代垃圾回收的实现原理。
简介
分代垃圾回收认为对象分为年轻代和老年代,其中年轻代对象很快就会被释放(比如临时对象),而老年代对象存在的时间比较长,不容易被释放,因此也就不需要经常去扫描老年代,只需要经常去扫描年轻代,等到年轻代垃圾回收的时候实在收不回对象,再进行一次全量垃圾回收。
原理
Lua 的 age 总共占用 3 Bit,刚创建出来的对象为 G_NEW
,当它活过一轮垃圾回收后,提升为 G_SURVIVAL
,若再活过一轮垃圾回收,则彻底进入 G_OLD
老年代,不在年轻代中扫描它。
1 |
这里面的 G_OLD0
是用于 Barrier forward,假设你创建了一个新对象,它本该是 G_NEW
但因为它被老年代对象引用,所以必须要强行将它改为老年代,否则会发生跨代引用,该新对象直接被清理掉。
同理 G_TOUCHED1
则是用于 Barrier back,假设你创建了一个新对象,然后放置在一个老年代的 table中,此时为了不频繁触发该 table 的 barrier,则将其修改为 G_TOUCHED1
,同时将其放置在 grayagain
链表中,这是因为老年代table是不会在年轻代的垃圾回收中被扫描到,但此时老年代又确实引用了年轻代对象,所以要将它放在一条特殊链表中,使其能在年轻代中被扫描到。
对 G_TOUCHED2
的理解就更为简单,前面我们知道新对象需要两轮年轻代垃圾回收才会进入老年代,为了不出现跨代引用,我们的老年代table也需要两轮年轻代的垃圾回收才能彻底放心的移出 grayagain
链表,因此 G_TOUCHED1
-> G_TOUCHED2
-> G_OLD
也是两轮垃圾回收。
而 G_OLD1
也是为了拖延 G_OLD0
真正变成 G_OLD
的时间,新对象就因为被老年代对象引用,它就直接变老年代是不合理的,需要让它也经历两轮年轻代垃圾回收再提升为真正的 G_OLD
。
实现
若能理解以上的 barrier,则可进入学习实现阶段。
假设当前 Lua VM 处于渐进GC模式,此时切入分代GC,只需要将未完成的GC完成,同时将所有已存在对象置为 OLD
。
1 | static lu_mem entergen (lua_State *L, global_State *g) { |
对 allgc
链表设置游标,认为所有对象都是旧的。
1 | static void atomic2gen (lua_State *L, global_State *g) { |
后续年轻代垃圾回收遍历 allgc
链表时,只需要遍历到指定位置(也就是之前设置的游标处)就可以结束本轮垃圾回收,大幅提高垃圾回收的速度。
1 | static GCObject **sweepgen (lua_State *L, global_State *g, GCObject **p, |