关于lua 垃圾收集不能全部收回内存的问题

我正做的游戏有如下现象,每若干帧打印lua的内存占用,观察:

若每一帧都执行完全垃圾收集,则lua内存稳定在10M左右。

若不执行垃圾收集,或者每帧只用很小的数值执行分步垃圾收集,则lua内存持续增长,大约翻倍的时候自动触发一次完全垃圾收集,内存量显著下降,但是不能回到初始值,比初始值高一点。这样内存量遵循 翻倍-回落 - 翻倍 - 回落 的循环逐步上升,最后达到数百兆的级别。

因为每帧执行完全垃圾收集内存是稳定的,所以我认为并不是lua代码中有存储泄漏。

因为lua代码中大量使用c++的导出对象(比如很多返回CCPoint的函数,local CCPoint 对象), 我怀疑的目标是,tolua++中是否有一个表用来记录管理所有c++对象,垃圾收集的时候,虽然c++对象都释放了,但是表格的自身数据结构(hash表)占用内存却越长越大?

我对tolua没有深入研究过,只停留在使用的层面,所以求对此熟悉的大侠指教。

tolua++确实有表格记录C++对象,但在C++对象释放后,表格里的记录会同步清理的。
请问你说的完全垃圾收集后,内存比初始值高一点,具体高的数值是多少呢?
lua内存使用翻倍时才GC是默认设置,你可以设置成1.5倍之类,看看情况又是怎样?

每5秒钟执行一次 print(collectgarbage(“count”)),结果是:

每帧collectgarbage(“collect”),输出
3745.837890625
3747.634765625
3748.708984375
3751.123046875
3751.123046875
3751.857421875
3751.857421875
3751.857421875
3751.857421875
3751.857421875
3751.857421875
3728.818359375

不每帧收集,输出
11575.3046875
12025.959960938
20766.952148438
23828.14453125
22916.5703125
22056.46875
26471.0078125
28604.63671875
37858.413085938
40913.987304688
45326.822265625
52934.458984375
62194.9921875
43205.646484375
44785.59765625
46146.874023438
47250.450195313
50033.172851563
53165.817382813
68467.118164063
71609.213867188
74731.666992188
77858.411132813
81000.059570313
108291.34375
92883.930664063
102116.78417969
105254.97949219
108331.75292969
80094.583984375
82145.341796875
83798.36328125
85262.135742188
86540.473632813
87724.313476563
88798.891601563
91162.266601563
94289.979492188
97375.631835938
100462.45019531
103589.44628906
106728.07128906
134350.10644531
137489.77636719
140628.83496094
143751.70605469
146972.97558594
150113.32714844
153251.52246094
155574.36914063
158712.56445313
161854.14453125
164978.55078125
217274.10546875
220401.96484375
223539.90820313
226627.12304688
229765.9296875
208234.23730469
235949.88964844
239088.08496094
242226.87402344
245324.42480469
248462.36816406
151760.35351563
154857.20507813

tolua中用来记录的表格,是c++中的容器还是lua中的表? 若是lua表,我怀疑当表中元素数减少时,表没有做re-hash,导致后台的数据结构占用内存没有释放。意思是说,假如lua源码用std::vector来实现表(当然仅仅是假如),当删除元素时,仅仅对这个vector做了erase(), vector占用的内存并没有减少;应该调用vector::shrink_to_fit()收回额外内存。

我又做了一个简化的例子:执行下面的lua

function update(delta)
–不停构造CCPoint临时对象
for i = 1, 100 do
local t = {}
table.insert(t, CCPoint(i, i))
end
–观察lua内存
mtime = (mtime or 0) + delta
if mtime > 5 then
mtime = 0
print(collectgarbage(“count”))
end
end

CCDirector:sharedDirector():getScheduler():scheduleScriptFunc(update, 0, false) --注册每帧执行的函数

就能观察到Lua内存上涨。

如果在update函数当中来一句 collectgarbage(“collect”),内存就是稳定的。

楼主,如果每帧都掉内存回收,这样内存是会比较稳定。但是我总感觉这样性能上会受影响。

如果每帧都回收内存,会有明显掉帧现象。所以我要来求助啊,没有足够时间自己去研究解法(需要研究tolua以及luajit的源码)。目前只能每帧collectgarbage(“step”, 0)对付一下,使得掉帧不明显,内存涨得不太快。但是内存终究还是要用完的,所以还是请求引擎开发组给出正式的解法。

mark … mark

tolua用的是lua的表。不过,如果是没有re-hash的原因的话,哪怕是每帧都GC,一样会有问题才对。等我作个测试,看有没有你所说的问题吧。另外,确认一下环境,你用的是quick还是cocos-lua呢?

我用的是quick 2.2.5。

好想知道后面发生了神马,坐等七月的测试

mark 看后续测试结果

to阳光七月:重现问题了吗? 能透露一下进展吗?

我做了个小实验:
在C++中写了一个lua内存分配函数,并开了一个容器记录所有lua内存申请和删除的记录:
std::map<void*, int> g_luaMemRecord;
void * MyLuaAlloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
if (nsize == 0)
{
free(ptr);
if (g_bLog)
{
g_luaMemRecord.erase(ptr);
}
return NULL;
}
else
{
void *p = realloc(ptr, nsize);
if (g_bLog)
{
g_luaMemRecord.erase(ptr);
g_luaMemRecord = nsize;
}
return p;
}
}
在 CCLuaStack::init(void) 中,将
m_state = lua_open();
改成
m_state = lua_newstate(MyLuaAlloc, NULL);
这样就接管了lua内存分配。

然后启动游戏,等游戏初始化完成后,先在调试器中将g_bLog设为true,开始记录内存操作。
执行下面的lua程序:
bigTable = {}
for i=1, 50000 do
table.insert(bigTable, CCPoint(i,i))
end
bigTable = nil
collectgarbage(“collect”)
然后在调试器中观察g_luaMemRecord,可以发现里边有2项大小为1572864的内存块。

如果不是创建CCPoint对象,而是改成
table.insert(bigTable, {1,2,3,i} )
则观察到g_luaMemRecord空空如也。

我猜想这个问题可能是tolua引入的,也可能是luajit内部的某种"只增不减"的机制,比如用vector来管理所有lightuserdata。
to开发组:期待你们的回复,即使回复说暂时不处理,也比杳无音讯要好!

这个大小的内存 看起来像是故意开辟的,试下直接下条件断点看看是哪里开辟了这两块内存

首先表示一下歉意。其实楼主的问题我是比较关注的。由于我的主要工作任务是quick V3版本的开发,原来的计划是在V3的beta版本发布后立刻来调试这一问题的。因为beta版本新增了一些重要的功能需求,虽然整个开发组都很努力,但由于人手确实有限,发布日期还是只能一再推迟,到现在仍然未能发布。目前目标是下周发布,但仍然需要努力。
今天看到楼主又继续做了调试,再次感觉到楼主的项目在解决这一问题的需求上是很急迫的。为此,我特意调整了一下工作安排,专门抽出周日晚上的时间来调试这个问题。

家里的电脑有点老了,调试这个问题需要反复编译,所以进展慢一些。不过,经过这一晚上的调试分析,初步还是有了一个结论。具体调试分析过程就不多说了,直接给出结果吧:

1.此问题仅存在于不是从CCObject继承的C++对象
例如,在楼主测试的例子中,如果用CCNode:create()来代替CCPoint(i, i),那么gc时内存是能够完全恢复的。
2.此问题不会造成C++对象的内存泄露
经过调试确认,楼主的例子中,虽然在gc时,lua内存没有恢复,但所有的C++对象均已经正常释放。这一点通过在构建函数和析构函数中的计数监视,反复验证是无误的。
3.问题基本确定根源在tolua的gc表
在cocos2dx中,如果要导出的C++对象不是从CCObject继承,则必须将此对象的指针存入名为tolua_gc的表中,此表存放于lua的注册表中。存放的方式为:tolua_gc=mt。其中ptr为对象的指针,mt为对象的元表。当Lua对userdata进行清理时,将会检查C++对象的指针是否存在于tolua_gc表中,如果存在,则根据元表获得C++对象的清理函数进行处理,C++对象将在此时被删除;最后会设置tolua_gc=nil。
经过调试,可以确定以上流程是完全正常处理了的。在gc后,所有的C++对象已经被删除,而且观察tolua_gc表中剩下的非nil值,可以发现tolua_gc=nil已经被执行。
那问题在哪里呢?在这一点上,楼主原来的猜测看来是正确的,也就是tolua_gc这个表应该是没有马上释放已经被赋为nil值的键值。根据我调试的结果,经过“固定次数”反复创建C++对象后,最后GC的时候,lua内存虽然没有完全恢复,但一定会稳定在一个相同的值;楼主的实验代码中,每一帧都进行gc的处理,其实就是“固定次数”为1的情况。可以推断原因是,在实验代码中,当一批C++对象被释放后,下一次再生成时,会被分配到和之前相同的内存指针,因此tolua_gc表不需要生成新的键值。
另外,继承了CCObject的对象,由于有自动释放机制,所以并不会使用tolua_gc表来记录,lua内存就能正常恢复,这也是问题应该出在tolua_gc表的一个有力证据。
如果阅读lua或luajit的源码应该能最终确认这一问题,但现在暂时没时间去看了。
4.暂时处理方案
如果前面的分析结果没有问题,那么,我对这个问题的建议是可以先不做任何处理。因为我估计tolua_gc表之所以没有被完全清理,恐怕是由于内存还充足,没能引起内部的清理。否则这一问题应该早就存在了,但这么多使用cocos-lua和quick-x的大型游戏,还未听说过由于这样而出现内存问题的。具体可以在真机上再作进一步的测试。

以上为初步结论,供参考。有问题仍可继续讨论。

昨晚调试完有些晚了,思路有点不太清晰,虽然给出了结论,但感觉不是太踏实。上班路上还在想这件事,早上头脑更清醒了些,仔细回想整个流程,突然发现自己忽略了一个本来很明显的问题。调试结果是没错的,但之前的结论错了,这里确实会有一个泄露。我马上着手进行修改,验证通过后会回来说明问题产生的细节。

mark:14::14::14::14::14:

非常感谢 @阳光七月 挑灯夜战Bug。辛苦了!期待你的好消息。

今天花了半天时间调试这一问题,发现原因比原来想象的还要复杂得多。
我增加了一些额外的代码,强制将gc表和ubox表这两个表进行重建,结果发现效果并不大。疑惑之余,我作了一个尝试,将循环中创建的所有CCPoint的都存入一个全局表中,这样每帧创建的对象都不会自动清理,而是一直加在表里。然后我在屏幕上创建一个按钮,在运行一段时间后按下,停止帧的流程,并将表中所有键值置成nil,全局表也置成nil,此时可以观察到所有CCPoint对象被清理,gc表和ubox表也已经被正常清理。由于这些清理工作都是在对象的GC操作中进行的,所以可以肯定lua的gc已经完全得到执行。但让人吃惊的是,这时打印显示,lua占用的内存还远远大于之前用局部表来存放对象时所用的内存!
得到这个结果我都不太敢相信,但反复测试确实如此。这说明无法被GC掉的内存实际上是lua自己的机制造成的(也有可能只是luajit是这样,有待验证)。结合每帧都清理测内存不会增大的情况,可以得到结论是,lua在使用了大量的变量后,即使这些变量被清理,也不一定会马上释放其空间,而是保留占用,不返回给native环境。仔细想想,确实这样处理其实是很高效的,一是加快了回收的速度,而继续分配变量时也不需要再去向native申请新的内存了。
另外值得一提的是,创建CCObject的子类的对象,由于能很快自动回收,所以效果和每帧都回收是完全一样的。
综上分析,我觉得之前提出的处理方案是可行的,即暂时不需要考虑这个lua内存占用的问题。因为这其实并不是内存泄露,而只是lua虚拟机充分利用资源来提高运行效率的结果。我相信在手机真机环境下,在lua虚拟机无法申请到更多内存的时候,它自己内部一定会自动启动gc,使得新的变量有地方存放。楼主可以在真机上测试一下,结果应该是lua占用的空间在达到一定大小后将会停止增长,而且这个占用的空间会比在电脑上测试时占用的空间小得多。
这个问题暂时就这样下结论吧。如果今后有新的发现,大家可以再继续讨论。

mark :2::2::2::2: