Lua会造成内存泄露的表征分析:
# 因素一:(实例型)实体资源的创建持有者和调用者,相互之间如果太过信任,那么对调用者就会形成过高的要求,创建者可以让任意的调用者进行任意的 create,调用者消费后以为创建者会管理(销毁),但其实并非如此,比如有这样一个实体管理器xxxManager,它有接口 createXXX/removeXXX, 那么,创建和销毁的权利都丢给了调用者,如果调用者光create而不remove,那么,xxxManager就会产生越来越多的xxx(xxx可真多 丫),从而产生了内存泄露
#因素二:逻辑层的角色数据如果没有跟随角色,将会导致前者和后者在生命周期上非不严格对应, 或者说角色对自己的数据的持有太松散,这样,如果角色在玩家下线后,即使被roleManager销毁了,但它所对应的数据,并没有得到销毁, 这样,也产生了泄露
# 因素三:singleton单实例对象,本身是在内存中持久存在的, 这样,从内存泄露的角度上看,它们对数据的持有的风险是很高的, 如果它们中的任意一个,有这样的一个接口addPlayer(player), 而在player下线时也没有进行即时的清除, 那么,泄露又产生了
#因素四:如果框架层接口太过于开放的话,也会给脚本层lua带来泄露的风险, 最典型的比如计时器,lua的某个方法启动了一个计时器而又忘了关掉它, 这就麻烦了.
因此, 虽然lua有gc,但是脚本中的游戏逻辑层,也会产生内存泄露,并且,泄露还很容易,泄露的地方还很多!
根源是什么?
这究竟是怎么回事呢? 由于以上的泄露,其实都是逻辑泄露(跟c/c++的泄露本质不同),那么我们可以从设计上,来探根源:
# 每个层自身职责的定义如果不够严谨,它们之间存在一些不必要的耦合的话,这里表现为(框架)层与(脚本)层,(实体)层与(逻辑)层,(全局资源层)与 (逻辑)层,以上的泄露就很容易产生, 举个简单例子:实体由实体层管理持有, 而逻辑层(在任何一个接口)却能直接的去创建,或者销毁一个资源实体,实体的生命横跨实体层和逻辑层, 生命周期发生了外泄, on the other hand,从设计的角度书,就是实体让两个层产生了耦合!
#实体资源本应该有的强依赖关系没有建立起来, 在游戏中最重要的就是角色和角色数据(比如任务),material,task,等,本身并不能独立存在,它们的生命周期完全依赖于角色的生命周期(原型 数据除外),所以,必须理清所有的实体的生命周期,已经它们之间的联系,该强耦合的强耦合.
#对逻辑层singleton的全局性对象持有实体资源的风险意识不够,根据上面一条,逻辑层全局子系统不能够直接持有任何一个实体(即使是原型).
设计上怎么做?
也就是说,我们在搭建脚本层的游戏框架上的时候,就首先对内存泄露有足够和清醒的认识, 在设计上, 做出更好的规划,让脚本层更健壮, 针对以上的原因,我们很容易的,有这样的做法:
#实体管理器如果是本身持有了实体,那么,就不应该开发create/remove接口,而是选择直接
#所有实体资源,主要是目前的玩家逻辑数据, 必须直接帮在role上,确保role的销毁比如会引发它们的销毁
#全局资源性的数据,可以考虑放在weak table中
监测机制的产生?
监测,就是必须存在这样一个机制:我们能够利用某些接口/命令,清晰全面的得知脚本层在是否存在,在哪里存在内存泄漏.毕竟,逻辑型泄漏的代码,很容易就可以写出来并且不能100%的杜绝,建立起这样一个机制,在分析游戏服务器端的健壮性,稳定性上,都是很有帮助的.
#计数法. 在垃圾收集中,计数法是比较原始的算法, 效率低,不能解决循环引用. 不过,如果我们把它用在实体管理器与实体,主实体与非主实体上,有可能可行,因为,这些对象间,并没有产生循环引用,另外,我们也通过在不同类型的类上采 用不同的时间间距,来达到比较好的性能. 也就是说,引入计数法,即可监测,其实还可以做垃圾收集
#对全局资源, 可以考虑引入mark-sweep算法/复制算法来管理,
如何让垃圾收集更加的高效?
lua gc 采用mark-sweep算法, 效率不高,并且好像没有看到有自己回收的地方, 如果在游戏应用层调用collectgarbage("collect"),不可避免会影响服务器的性能,所以, 我们可以对lua中的模块进行(javaGC类似的)分代, 不同代的数据使用不同的保存,封装和清除策略,保证在最大效率的情况下准确的完成垃圾收集!
BTW,这里有另一篇对Lua的内存泄露文章,供大家开阔视野:
本 文主要介绍某项目脚本(lua)部分内存泄漏的查证与处理过程,希望对大家有点 帮助。需要说明的是,lua本身并不存在真正的内存泄漏,只是因为使用上面的原 因导致无法gc,从而导致逻辑上的泄漏:)。 参考GCObject的声明可以发现,lua中的复杂数据类型变量的传递都是基 于引用的。当lua从根开始gc扫描的时候,只要还有一个地方有对此变量的引用,那 么这个变量就不会被collect。这种情况造成的危害取决于多大程度上依赖于引 用,如果有适当的间接层/弱引用来隔离这个问题,可能问题会有所缓解。 以下是一些常见的错误引用情景: 1. 本应该local 的变量进入global空间或者module空间了(忘记写local),如果 这是一个table/function/udata等类型的变量的话,非常不幸的,这个变量将不会 被正确gc了 ----除非你再显式的释放。这是非常容易犯的错误,一直在想为什么 lua变量不是默认local呢? 当然这个话题会引发另外一场争论。 local function test_user(id) userobj = get_user_by_id(id) --这里总是会有一个玩家对象泄漏 print("only test", userobj:get_name()) end 2. c/c++部分调用的lua_ref是否有正常lua_unref释放? 通过 debug.getregistry()可以查到这些ref. 3. 其他各种各样的实际bug造成的泄漏。 当怀疑系统有泄漏以后,我们可以怎么查到这些泄漏呢?我强烈建议大家建立一 个weak table, 把你所有创建过的能够称之为资源的,包含但不限于“战斗对象, 玩家,npc,物品,场景,邮件”等等对象全部扔到这个table里面。当你知道玩家 已经下线、战斗已经销毁了,但通过连续的强制full gc以后weak table里面还有 这个变量,这就证明了这个变量的引用没有被完全释放,于是问题就被发现了,我 们又有事情干了@_@。 知道有泄漏是比较容易的,能够完全揪出来就不是很容易了。是的,它究竟在哪 儿呢? 一开始在此项目里面也是先发现比如某npc泄漏了,然后就去查代码,看看 究竟哪个地方写得不对。这种方式效率极低,基本上查不到什么问题。在迟一点的 时候才使用现在的方案:从_G深度遍历所有的table、metatable、funciton's upvalue、function's env、registentry(lua_ref)。 目前所知的所有引用必定存 在于这几个空间, 遍历完成以后一定可以找到那个“迷失了的引用”。 这种方式在 脚本层就可以完成所有事情,甚至你可以在运营环境中在线查证,其遍历的速度 是非常快的,但内存开销非常大(:,可以考虑一边遍历一边gc,当然还要记得 避免重复搜索。 在应用此方案以后,此项目解决了脚本中所有的泄漏问题。 一点总结:1.如果系统性能还能够承受的话,建议不要直接引用对象,可以多做 一层间接层。2.lua里面的弱引用是非常有用的。3.比较大的物理内存是必要的, 这可以为大家查证问题争取足够多的时间:) 4.可以把查找泄漏的部分写入到关机 逻辑里面,每次关机的时候自动查找泄漏,然后出具报告。Lua的内存监测和回收
Lua 内存是自动收集的, 这点跟Java类似, 不被任何对象或全局变量引用的数据,将被首先标记为回收,不需要开发者做任何事情.但是,正如Java也会有内存泄露一样, Lua也会有, 只不过,跟C++的不同,它是由于代码执行所装载的资源,并没有被彻底销毁而导致,其中,最臭名昭著的就是不小心把局部变量声明成了全局变量(忘了加 local修饰符)。 类似这样造成的内存泄露, 跟任何其他语言的内存泄露一样,容易产生,却难以察觉, 给开发的应用带来潜在的很大隐患.
那么, 有没有一些有效的解决办法, 来解决这个这个隐患呢, 答案就是collectgarbage. collectgarbage就是开放给Lua开发人员, 用于监听Lua的内存使用情况(collectgarbage("count")), 同时,它还提供了collectgarbage("collect"),允许在适当的时候进行显式的回收.
现在,通过测试代码来看看,如何玩转collectgarbage.
首先,为了有明显的对比, 先来看没有产生泄露的情况, 运行以下的test1(代码如下):
运行结果如下:
这里看到, 被local 声明的colen加了5000数组, test1调用后, 内存增加了大概300K(25906K-25620K).现在,我们来做内存回收(调用mem函数, 代码如下):
运行结果:
( 为了保证内存的稳定,以上注意mem被调用了多次, 再第2次, 可以看到内存开始下降, 最后,大概在25618K稳定下来)好了, 从最初的25620K, 到回收后的25618K, 两者并没有发生变化(还少了2K,嘿嘿, 这应该是误差了), 也就是说,函数test1的执行,并没有产生无法回收的内存,没有泄露出现.
好了,现在运行有泄露的test2(代码如下), test2跟test1相比,只有一处不同:就是colen被误声明为全局:
结果:
也就是说,内存也在25906K,跟test1几乎是相等, 好了,现在再调用回收(mem)函数,产生结果如下
为 了保证函数回收被执行,这次,总共调用了7次mem函数(看以上打印行数), 那么,从上面的结果我们看, 很不幸, 从第1次,到最后第7次, 内存都还是稳定在25905K左右, 也就是说, 跟调用test2前相比,即使Lua进行了内存回收, 内存却不会将下来 看来, 这300K(25906K-25620K)内存, 由于已放到了全局函数中,是永远没有机会被回收到了!
总结一: 如何监测Lua的编程产生内存泄露:
1. 针对会产生泄露的函数,先调用collectgarbage("count"),取得最初的内存使用
2. 函数调用后, collectgarbage("collect")进行收集, 并使用collectgarbage("count")再取得当前内存, 最后记录两次的使用差
3. 从test1的收集可看到, collectgarbage("collect")被调用,并不保证一次成功, 所以, 大可以调用多次
总结二: 如何避免Lua应用中出现的内存使用过大行为:
1. 当然是代码实现不出现泄露, (废话*&%$()
2. 在测试中,其实还发现, Lua中被分配的内存,其实并不会自动回收(个人估计要么就是Lua虚拟机没有做这个事情,要么就是回收的时机是在C层), 所以, 为了避免内存过大, 应用的运行时,可能需要定期的(调用collectgarbage("collect"),又或者collectgarbage("step"))进行显 式回收。