Lua5.4 分代gc 的理解
1.为什么使用的是 GC 而不是 RC?
可以这样说,GC (garbage collection) 把 RC (reference counting) 中那些短期对象的销毁代价转嫁到了一次性的标记清除过程。这把逻辑处理和资源管理正交分解了。这种被分解的问题,会随着硬件的进步更容易提高性能(比如多核的发展)。但是,在较小规模的软件或独立模块中,这个优势并不会太明显。反而 GC 本身远高于 RC 的复杂性,会成为其软肋。
参考云风的博客: https://blog.codingnow.com/2008/06/gc.html
2.为什么要使用分代gc?
根据局部性原理(计算机能发展到现在阶段,最主要的就是依赖局部性原理,例如虚拟内存),一个对象在某个时间点被访问了,那么接下来的一段时间内会重复的访问该对象。
那么如果使用Lua5.2中的四色标记法,每次都会将此轮gc中未被使用的对象回收掉,但是这些对象可能是在上一轮存活过的,那么极可能在接下来的一段时间还会多次访问到(即使这次没访问),那么就不应该那么快的回收掉,从而下次还需要申请内存来创建。
3.Lua5.2已经有第一版本的分代gc了,它Lua5.4的分代有何不同?
最主要的区别在于,Lua5.2版本的gc的老年代对象的判定是存活过一轮即可成为老年代,从而导致大量的临时变量都成为了老年代而不会在下一次局部gc的时候回收掉,这样的话内存增长跟分步gc没什么区别,所以会比较频繁得切换回分步gc进行全量的gc;而在Lua5.4中,只有当一个对象成功得活过两轮gc才会被标记未老年代,这样有效得避免了临时变量的干扰。
4.分代GC主要的目的是实现什么作用?
最主要的功能是,避免每次都进行全量扫描,只对新生代的对象进行扫描,这样可以降低扫描成本从而减少GC成本。
5.开启分代GC模式:
在初始化完lvm时,显示调用 lua_gc(L, LUA_GCGEN, 0, 0); 即可。
6.分代GC的整体架构:
分代GC主要分两个GC过程,一个是完整的GC(即major GC),其二是局部GC(即minor GC)。
当调用 lua_gc(L, LUA_GCGEN, 0, 0); 时会执行luaC_changemode函数进而进入到分代模式中,将会先进行分步模式下的gc扫描以及atomic扫描,之后会将所有存活下来的所有对象都设置为 G_OLD 状态(这个gc步骤即为 major GC,在进行major GC 前需要先进入到分步GC模式下进行初始化并且执行扫描)。
当major GC 完成并判断后进入到分代GC模式,当触发下一轮GC时,会先判断上一轮完整GC到当前时间所增加的内存大小,从而判断是否进入到局部GC(youngcollection,具体数值将在文中后续给出),youngcollection会将此前的对象链表中的状态进行更新以及将新生的对象并且不能达的回收掉。
7.在分代GC中,是怎么触发下一轮GC的?
不只是在分代GC中,在分步GC中,也是使用一个字段来判断是否需要执行新一轮的GC,在global_State 中的 GCdebt 字段,如果在申请内存时发现此字段的值大于等于0时则会进行一次GC。
特别的,在 global_State 中的 totalbytes 字段,在分代GC中用来选择执行 major 还是 minor GC。
具体的使用将在下文提到。
8.在分代GC的执行过程中,major GC 与 minor GC 是怎么配合的?
接下来讨论 totalbytes 字段,文中的所有变量都使用lua默认的值。
第一步:具体的配合操作在 genstep 中,首先会先判定上一轮的young gc是否为 bad collection。
第二步:如果不是 bad collection,则会判断上一轮设置的 totalbytes 字段是否大于 majorbase + majorinc (这里的majorbase 是指上一轮完整GC 所存活下来的对象总数量,majorinc 默认的值是majorbase, majorbase + majorinc 则表示内存增长大于上一轮完整GC的对象数量,如果是则需要进行一次 fullgen(major GC),如果此次 fullgen 回收掉了一半的增长量(即回收了majorinc / 2 对象数量),则下一轮GC继续进行分代GC(此时会将 GCdebt 设置为20%的值,表示下一次触发时机为内存增量达到20%时),如果此次 fullgen 没有回收掉一半增长量则表示当前的是 bad collection(此时会将 GCdebt 设置为100%);如果 totalbytes 字段小于 majorbase + majorinc 则进行一次 young gc(同 GCdebt 设置为20%)。
如果是一次 bad collection, 则会进行一次 stepgenfull 操作,此操作会重复执行 major GC 操作,只在当前的内存增量小于 1/8 totalbytes 时,下一次触发gc才会进入第一步中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | static void genstep (lua_State *L, global_State *g) { if (g->lastatomic != 0) /* last collection was a bad one? */ stepgenfull(L, g); /* do a full step */ else { lu_mem majorbase = g->GCestimate; /* memory after last major collection */ // #define getgcparam(p) ((p) * 4) // #define setgcparam(p,v) ((p) = (v) / 4) // #define LUAI_GENMAJORMUL 100 // #define LUAI_GENMINORMUL 20 lu_mem majorinc = (majorbase / 100) * getgcparam(g->genmajormul); // (majorbase / 100) * LUAI_GENMAJORMUL = majorbase if (g->GCdebt > 0 && gettotalbytes(g) > majorbase + majorinc) { // 上一次完整gc到现在期间,内存增长翻倍以上 lu_mem numobjs = fullgen(L, g); /* do a major collection */ // 会切换分代与分步模式,为了避免多次进入这里导致切换频繁,所以在下面加了"bad collection"标记。 if (gettotalbytes(g) < majorbase + (majorinc / 2)) { // 重新扫描一次分步模式下的对象数量,如果这次扫描后的新增的对象数量少于扫描前的数量的一半则继续分代的局部gc /* collected at least half of memory growth since last major collection; keep doing minor collections */ setminordebt(g); // 设置下一次minor gc的时机,默认为当前总对象数量的20%新增时触发 } else { /* bad collection */ g->lastatomic = numobjs; /* signal that last collection was bad */ setpause(g); /* do a long wait for next (major) collection */ // 默认情况下是新增量达到上一次存活下来的对象数量的一倍时 } } else { /* regular case; do a minor collection */ youngcollection(L, g); setminordebt(g); // 设置下一次minor gc的时机,默认为当前总对象数量的20%新增时触发 g->GCestimate = majorbase; /* preserve base value */ } } lua_assert(isdecGCmodegen(g)); } |
9.分代GC中涉及到的状态变化,各状态的理解
old的定义是状态大于 G_SURVIVAL
G_NEW:本次cycle创建的新对象(没有引用任何old对象)
G_SURVIVAL:上一轮cycle创建的对象 -- 只活过一轮,下一次如果是白色的话,仍然会被回收。
G_OLD0:表示本次cycle创建的新对象,但是引用了old对象,需要barrier操作。为什么不直接设置成old对象?同G_SURVIVAL,是实现的问题。
G_NEW,G_SURVIVAL,G_OLD0表示都不是老对象
G_OLD1:表示作为老对象第一次存活了整个gc过程 -- 为什么需要G_OLD1(从G_SURVIVAL或者G_OLD0变成G_OLD1,而不是直接从G_SURVIVAL或者G_OLD0变成G_OLD)?因为如果节点A现在是在G_SURVIVAL或者G_OLD0,在同一个cycle中转成old对象前,有一个子节点B引用了A节点(例:A[B]=C),这时候因为A节点仍然不是old,所以不会触发到barrier与barrier_back操作,所以节点B仍然是G_NEW,到最后,A节点直接转变成G_OLD的话,B节点转变成G_SURVIVAL,因为在youngcollection中不会对g->reallyold链表进行markold操作,所以在下一次gc中,如果B节点不可达(即是白色)的话,那么在这次gc中会被释放。虽然可以改成old也传播,但是这样的话就破坏了规则(会对所有的G_OLD都遍历,这样就达不到缩减成本的目的了)。
G_OLD:表示真正的old对象,不会被回收 -- 作为老对象需要2次gc过程,作为新创建的对象需要经过3次gc才会到达此阶段。
G_TOUCHED1:标记位G_OLD的对象在这次gc barrier_back的状态 -- 新touch的对象,需要进入到grayagain中
G_TOUCHED2:标记为G_OLD的对象在上一次gc barrier_back的状态前进到touched2 -- 分代gc结束时,从G_TOUCHED1转成G_TOUCHED2,并设置为黑色,仍然存在于grayagain中,以便下一轮再次有新对象使该对象 barrier_back 时,只需要修改为灰色(使用计算机的局部性原理,这次用到的东西,下一次可能还会触发,避免下次触发时进行链表的操作)。如果下一次没有触发为touched1则变成G_OLD。在correctgraylist中会对touched2跟old的对象从grayagain删除。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)