GC是分步执行的,先来思考几个问题:
1、什么时候触发执行?
Lua是用新增使用内存量达到一定数字时触发GC执行
2、这个新增使用内存量是怎么定的?
Lua使用GCdebt变量来预设下次触发完整GC时机和触发下次单步GC时机 (时机:新使用内存增量时间间隔)
3、每次单步执行执行多久?
这个时间也是与GCdebt有关
4、Lua可回收对象有三种颜色,白色、灰色和黑色.
白色表示没有被标记的对象,白色有两种每次完整GC后切换;
灰色表示标记过的对象,但可能引用未标记的对象;
黑色表示标记过的对象,并且它引用的对象都标记过;
GC在繁殖和原子阶段有个不变式:黑色对象不能指向白色对象
i:什么时候清理未标记的白色对象,切换白色?
原子阶段执行完后切换白色,之后阶段就是清理
ii:怎么保证上面的不变式不变?
barrier函数
一、global_State 部分变量说明
1、GCdebt 没有被回收器补偿的分配字节,预估内存借债,新申请的内存超过此值,则触发GC。
2、GCestimate 使用中的非垃圾内存的估值(与当前内存实际分配数量大约相等)
3、totalbytes: 当前已经分配的内存+预估要分配的内存, 真实占用内存为totalbytes + GCdebt(可正可负).
其中预估要分配的内存相当于GC借的债,一旦真正分配的内存超过借债时,就要触发GC了。
触发一次GC若不能完整跑一遍GC流程则再借债时根据gcstepmul调整下次单步GC的粒度;若触发一次GC完整
跑一遍GC流程则再借债时根据GCestimate和gcpause确定借债额度(这个值会影响本次完整GC和下次完整GC之间的间隔,即是每次完整GC的频率)
4、gcstepmul控制本次单步gc时间和触发下次单步GC内存增长值
STEPMULADJ = 200
单步GC函数如下:
/*
** performs a basic GC step when collector is running
*/
void luaC_step (lua_State *L) {
global_State *g = G(L);
l_mem debt = getdebt(g); /* GC deficit (be paid now) */
if (!g->gcrunning) { /* not running? */
luaE_setdebt(g, -GCSTEPSIZE * 10); /* avoid being called too often */
return;
}
do { /* repeat until pause or enough "credit" (negative debt) */
lu_mem work = singlestep(L); /* perform one single step */
debt -= work;
} while (debt > -GCSTEPSIZE && g->gcstate != GCSpause);
if (g->gcstate == GCSpause)
setpause(g); /* pause until next cycle */
else {
debt = (debt / g->gcstepmul) * STEPMULADJ; /* convert 'work units' to Kb */
luaE_setdebt(g, debt);
runafewfinalizers(L);
}
}
luaC_step过程:
1) 计算本次单步gc需要遍历的对象字节数: debt = (debt / STEPMULADJ + 1) * gcstepmul
2) do {} while循环遍历对象,work为遍历到的对象所占字节. debt值要小于等于-GCSTEPSIZE(64位linux下值为2400)才会停下.
3) 计算触发下次单步GC内存增长值:debt = (debt / g->gcstepmul) * STEPMULADJ
如果gcstepmul 为200, 则基本是内存额外增量为多少,本次gcstep额外就要遍历多少内存
如果gcstepmul 小于200,则要遍历的额外内存少,触发下次GC的内存增涨量变大(gcstep的时间较短,一次完整GC每步之间的间隔较长)
如果gcstepmul 大于200, 则要遍历的额外内存多,触发下次GC的内存增涨量减少(gcstep的时间较长,一次完整GC每步之间的间隔较短)
5、gcpause控制每次完整GC间的间隔(即完整GC频率)
threshold = GCestimate / 100 * gcpause(默认200)
debt = totalbytes - threshold ~~= totalbytes * (1 - gcpause / 100)
如果gcpause为200, 则当内存分配超过当前内存一倍时才触发下次完整GC
如果gcpause <= 100,则基本每完整GC跑完后就会开始下一轮完整GC
说明gcpause值越小完整GC频率越高
6.GCmemtrav 单步中GC遍历的内存记录
---------------------Lua中所有对象一定在7到10中某个链表上---------------------
7、allgc 所有没有被标记为自带终结器的对象链表
8、finobj 所有被标记为自带终结器的对象链表
新增元素的地方:
1)第一次设置table的元表且元表中含有__gc方法,此table对象会从allgc链表中移除
2)第一次设置userdata的元表且元表中含有__gc方法时,此userdata对象会从allgc链表中移除
9、tobefnz 将要被释放的对象链表
10、fixedgc 不会被回收的对象链表
11、gray 常规灰色等待被访问对象链表
12、grayagain 在原子阶段必须被重新访问的灰色对象链表
包括:在白色barrier中的黑色对象;在繁殖阶段的所有类型弱表;所有线程
新增元素的地方:
1)繁殖阶段遍历弱值表时
2)繁殖阶段遍历弱key表时
3)遍历线程时
4)将黑色结点变灰色向后barrier时
---------------------13到15链表仅仅在原子阶段有效---------------------
13、weak 弱值表对象链表
新增元素的地方:
1)非繁殖阶段遍历弱值表含有可能需要清理的值时
14、ephemeron 蜉蝣对象(弱key表)链表,含有白色->白色结点
新增元素的地方:
1)非繁殖阶段遍历弱key表时有结点的key和value都为白色
15、allweak 有弱键和/或弱值将被清理的表
新增元素的地方:
1)非繁殖阶段遍历弱key表时含有可能需要清理的key且其value标记过
2)遍历表时表是弱key且弱值型
二、GC各阶段具体执行步骤
标记函数: 标记对象为灰色,这里仅把部分添加到gray链接中等待繁殖阶段一个个遍历,因为这些对象引用到的对象可能比较多,为了避免访问元素过多耗时
1) 短、长字符串对象直接标记为黑色
2) 用户数据对象: 标记元表,将此对象标记为黑色,标记设置的用户对象
3) Lua闭包和C闭包都添加到gray链表中
4) 线程添加到grayagain链表中
5) 函数原型添加到gray链表中
1、GCSpause 暂停阶段
重启垃圾回收:
gray grayagain 链表都置为空
weak allweak ephemeron 都置为空
标记主线程 标记注册表
标记基本类型的元表
标记上个循环留下来的将要被终结的对象
2、GCSpropagate 繁殖阶段
遍历标记: 访问灰色对象
将灰色对象标记为黑色,表示此对象及其引用的对象都被标记过,并从灰色链接中移除
1)表对象的遍历:
1:标记元表
2:表为弱表,则将表再标记为灰色
i: 表为弱值型,则仅标记哈希部分值不为空的key对象.若当前在繁殖阶段则将此弱表链接到grayagain上,
等待原子阶段再次被访问;若在原子阶段且表有数组部分或哈希部分有值为白色则将此表链接到weak上.
ii:表为弱key型,则仅标记结点值对象.若在繁殖阶段则将弱表链接到grayagain上;若在原子阶段且哈希部分有白色key指向白色值则将弱表链接到ephemeron上;若在原子阶段且哈希部分有白色key则将弱表链接到allweak上.
iii:表为弱key弱值型,则将表链接到allweak上.
3:表为强表,则标记所有结点的key,value
2)Lua闭包的遍历:
1:标记原型
2:在原子阶段标记所有上值,在非原子阶段仅标记闭合的上值.(开放的上值指向的值在线程中,因此那些值在非原子阶段线程被遍历时会被标记.
且那个阶段上值在线程中不能被改变,不也会被再次遍历)
3)C闭包的遍历:
1:标记上值
4)线程的遍历: 先将线程从gray链中移除,并添加到grayagain链上,把颜色变为灰色.
1:标记栈
2:若当前为原子阶段,则置栈顶到栈上限元素为nil;若不在开放上值线程链表上且有打开的上值,则将此对象链接到开放上值线程链接中
3:若当前不是原子阶段且GC类型不是KGC_EMERGENCY,则收缩线程的栈
5)函数原型的遍历:
1:如果有上次创建的闭包缓存,则置缓存为NULL
2:标记源文件信息
3:标记常量
4:标记上值名字
5:标记内嵌的函数原型
6:标记局部变量名
3、原子阶段
1)首先确保gray链表为空,不为空则遍历gray上所有元素
2)置当前GC状态为GCSinsideatomic
3)标记当前运行的线程
4)标记注册表
5)标记基本类型的元表
6)标记上值:
1:若线程不为灰色或没有上值,则从有开放上值的线程链表(twups)中移除,并标记所有触碰过的上值
7)再次遍历gray上所有元素
8)遍历grayagain链接上所有元素
9)循环遍历浮游链表上弱key表(因为上面的步骤可能导致key变为黑色),直到没有需要标记的结点,最后浮游链表上的元素部分仍然是之前链表上的元素
--------- 至此所有可被访问的强对象都被标记了 ----------------
10)清理weak链表上弱表中可能需要被清理的值
11)清理allweak链表上弱表中的可能需要被清理的值
12)把finobj链表上没有被标记的对象移动到tobefnz链表上
13)标记tobefnz链表上的元素
14)再次遍历gray上所有元素
15)执行第9步
--------- 至此所有复活的对象都被标记了 ----------------
16)清理ephemeron链表上弱表中的可能需要被清理的key
17)清理allweak链表上弱表中的可能需要被清理的key
18)清理weak链表上12)步之后新增弱表中的可能需要被清理的value
19)清理allweak链表上12)步之后新增弱表中的可能需要被清理的value
20)清理字符串缓存
21)切换白色
22)进入清理阶段
用sweepgc记录下次该清理哪个元素
分析:执行10,11步是因为即使弱key表上结点的value可能在12到14步被标记,但实际也是将要被终结的对象,因此要把这个弱值置为nil
4、清理所有对象阶段
5、清理finobj链表上对象阶段:原子阶段已经把白色对象转移到tobefnz上,这里仅仅是切换白色
6、清理tobefnz对象:切换白色
7、清理结束:将主线程切换白色
8、调一些在tobefnz上对象的终结器
GCTM调用对象自定义终结器函数:
1)先将对象从tobefnz移动到allgc上,重置终结标记,如果是清扫阶段,则将对象置为当前白色。这里仅把对象移动到allgc上而不是直接清理,会在下一个GC清扫阶段
2)调用自定义的终结器函数
三、barrier函数
luaC_barrier_函数说明:
p为黑色,v为可回收对象,并且v为白色
1)若当前GC是需要保持不变式状态(繁殖或原子阶段),则标记v对象;
2)否则为清扫阶段,则把p标记为白色,因而说此函数是向前barrier,直接把p跳过清扫时白色切换
调用地方:
1)lua_copy:若被复制到的位置是C闭包的上值
2)lua_setuservalue
3)lua_setupvalue:若是为C闭包设置上值
4)addk 向函数原型中添加常量时
5)lua_setmetatable
6)registerlocalvar 向函数原型注册局部变量
7)newupvalue 解析时函数原型添加新的上值名
8)addprototype 解析时函数原型添加新的内嵌函数原型
luaC_barrierback 函数说明:
p为黑色,v为可回收对象,并且v为白色
将p变为灰色并链接到grayagain链表上
调用地方:
1)lua_rawset
2)lua_rawseti
3)lua_rawsetp
4)luaH_newkey
5)luaV_finishset
6)执行OP_SETLIST
7)luaV_fastset
luaC_upvalbarrier 函数说明:
uv的值是可回收的且是闭合的
若是繁殖或原子阶段则标记uv的值
调用地方:
1)lua_load
2)lua_setupvalue:若是为Lua闭包设置值
3)lua_upvaluejoin
4)luaF_close
5)OP_SETUPVAL
分析:由于对table操作频率比较高,因此触发到barrier条件时使用向后barrier。把表添加到grayagain链表上,等到后面重新遍历一次。
其他barrier操作涉及的情况比较简单,操作频率理论不高,因为是向前barrier