Lua内存泄露检测原理和weak_table弱引用

来源:互联网 发布:js对象排序函数 编辑:程序博客网 时间:2024/06/08 16:07

lua内存泄露

首先第一点,lua中的内存泄露和我们所说的c/c++中的内存泄露本质上是不一样的。lua中有垃圾回收机制(GC),所以理论上是不会有内存泄露的。当它进行GC的时候,会从根部开始扫描所有的对象,如果某个地方对这个对象还有引用,就不会把这个对象内存collect,这个对象就没有被GC。所以lua中的内存泄露是指那些:已经没有被使用了,但外部依然还有引用存在的对象。

--函数中应该被申明为local的对象忘记加locallocal function test()     testTable = {} --这个testTabel会被存放在<span style="background-color: rgb(255, 255, 0);">全局表_G</span>中,GC时由于此对象还有引用存在,所以这里总是会有一个table泄露。     local mt = {} --mt加了local修饰,函数调用完后,引用也不复存在了,GC时会被回收。     setmetatable(testTable, mt) end

 以下是一些常见的错误引用情景:
 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,当然还要记得避免重复搜索。 在应用此方案以后,此项目解决了脚本中所有的泄漏问题。


检测原理

lua中支持垃圾回收机制的对象有五种:string,table,function,full userdata,thread而他们的引用直接或间接的保存到lua_state对象,_G全局表,Registry注册表,global_state->mt中。

在脚本中:

运行的lua脚本本身就是lua_state。_G就是_G全局表。Registry表可以用debug.getregistry获取。global_mt可以用debug.getmetatable获取。所以我们就可以在脚本层次实现内存泄露的检测模块。

在搜索时需要注意的几点:

table 额外搜索metatable,若metatable中的__mode取值为”k"、"v"或者”kv"需特殊处理(补充中有说明); function 额外搜索 enviroment,也是一个table; 额外搜索upvalues,这个可以是任何类型。由于userdata在script层次不能被修改,所以搜搜他的metatable吧thread对象就是coroutine对象,在script中一般都不会创建多个coroutine,所以在脚本中没搜索它。若是需求的话,获取到它的线程函数,然后再按照第2步操作就可以了。

搜索流程图(_G表)

\

检测泄露之前,先搜索一下所有的对象,保存好起始的内存状态,在程序执行之后执行几次GC操作,然后再进行一次搜索,对比两次的结果,多出来的那些就有可能是内存泄露了。

__mode 赋值为 "k", "v"或者”kv",表示保存在它中的键或值或键值都是一种弱引用状态。若一个对象的所有引用都是弱引用了,那么这个对象也会被GC回收掉,所以对应的weak表中此对象的入口就没有了。

所以我们可以用另外一种实现:就是把用户自己创建的资源对象统统都丢到weak表中,运行完程序后强制GC,然后去查看weak表,若表中还保存着那个对象,就意味着这个对象还有外部引用(相对弱引用我们就叫它为强引用吧),资源没有被GC掉,所以我们可以说这个对象很有可能是内存泄露了。( 为了发现内存泄漏,我们可以创建一个全局的弱引用table,使其key为弱引用,然后在每次创建那些可能存在泄漏的对象的时候,都放入这个table,让其作为key,value通常我会用当前时间。由于弱引用的性质,如果其他引用都消失了,那么在弱引用table中对这个对象的引用也会消失(变成nil),反之,只要还有其它任何一个引用存在,这个弱引用表中对这个对象的引用就继续存在。依赖这个特性,当程序已经跑过释放对象的逻辑后,如果这个表中还存在有这个对象的引用,那么这个对象肯定就是泄漏了。)

Lua垃圾回收算法

Lua的GC算法使用的所谓“Mark And Sweep”算法。简单的理解,这个算法将GC分为两个阶段,一个是标记(mark)阶段,这一阶段将所有系统中引用的对象都逐一标记而在清理(sweep)阶段,将把在mark阶段中没有被标记的数据删除。

在Lua中,使用几种颜色来区分不同的结点:

white:白色表示没有进行过标记的节点

gray:灰色表示已经进行过标记的节点,但是与它相关联的节点还没有进行过标记。

black:本节点和与之关联的节点都已经被扫描标记过了。通常会出现有关联数据的,包括有Table,upvalue等数据类型。

垃圾收集器函数

collectgarbage函数提供了多项功能:停止垃圾回收重启垃圾回收强制执行一次回收循环强制执行一步垃圾回收获取Lua占用的内存,以及两个影响垃圾回收频率和步幅的参数。collectgarbage(opt,[,arg])

"stop"

停止垃圾收集器,如果它的运行。

"restart"

如果垃圾收集器已经停止,将重新启动它。

"collect"

执行一次全垃圾收集循环。默认执行此操作

"count"

返回当前Lua中使用的内存量(以KB为单位)

"step"

单步执行一个垃圾收集. 步长 "Size" 由参数arg指定 (大型的值需要多步才能完成),如果要准确指定步长,需要多次实验以达最优效果。如果步长完成一次收集循环,将返回True

"setpause"

设置 arg/100 的值作为暂定收集的时长;并返回设置前的值。默认为200

控制了收集器在开始一个新的收集周期之前要等待多久。 随着数字的增大就导致收集器工作工作的不那么主动。 小于 1 的值意味着收集器在新的周期开始时不再等待。 当值为 2 的时候意味着在总使用内存数量达到原来的两倍时再开启新的周期。

"setstepmul"

设置 arg/100 的值,作为步长的增幅(即新步长=旧步长*arg/100);并返回设置前的值。默认为200

控制了收集器的工作速度,这个速度是一个相对于内存分配的速度。更大的数字将导致收集器工作的更主动的同时,也使每步收集的尺寸增加。 小于 1 的值会使收集器工作的非常慢,可能导致收集器永远都结束不了当前周期。 缺省值为200%,这意味着收集器将以内存分配器的两倍速运行。

function test1()    collectgarbage("collect")--为了有干净的环境,先把可以收集的垃圾收集了    collectgarbage()--为了保证内存的收集的相对干净,及内存的稳定,要执行多次收集    print("now,Lua内存为:",collectgarbage("count")) -->205.7158203125 KB    local colen = {} --现在是局部变量    for i=1,5000 do        table.insert(colen,{})    end    print("now,Lua内存为:",collectgarbage("count"))-->860.4111328125 KB    --创建5000个table,内存增加了655 KBendfunction collect1()    print("now,Lua内存为:",collectgarbage("count"))-->608.060546875 KB    collectgarbage()    collectgarbage()    print("now,Lua内存为:",collectgarbage("count"))-->204.8408203125 KB    --最后与一开始只差只有1KBendfunction test2()    collectgarbage("collect")--为了有干净的环境,先把可以收集的垃圾收集了    collectgarbage()--为了保证内存的收集的相对干净,及内存的稳定,要执行多次收集    print("now,Lua内存为:",collectgarbage("count")) -->205.7158203125 KB    colen = {} --现在是全局变量    for i=1,5000 do        table.insert(colen,{})    end    print("now,Lua内存为:",collectgarbage("count"))-->619.826171875 KB    --创建5000个table,内存增加了414 KB;这些增加的内存,由于已放到了全局函数中,是永远没有机会被回收到了!endfunction collect2()    print("now,Lua内存为:",collectgarbage("count"))-->596.7822265625 KB    collectgarbage()    collectgarbage()    collectgarbage()    print("now,Lua内存为:",collectgarbage("count"))-->489.189453125 KB    --最后内存增加了284KB(489-205)end

垃圾回收器有两个参数用于控制它的节奏:

第一个参数,称为暂停时间,控制回收器在完成一次回收之后和开始下次回收之前要等待多久;

第二个参数,称为步进系数,控制回收器每个步进回收多少内容。粗略地来说,暂停时间越小、步进系数越大,垃圾回收越快。这些参数对于程序的总体性能的影响难以预测,更快的垃圾回收器显然会浪费更多的CPU周期,但是它会降低程序的内存消耗总量,并可能因此减少分页。只有谨慎地测试才能给你最佳的参数值。

[转自]http://www.2cto.com/kf/201502/377646.html

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Lua教程之弱引用table

这次要介绍的内容比较少,就一个——弱引用table

1.无法超越人类智慧的智能——自动内存管理的缺陷

我们都知道,Lua是具备自动内存管理的,好吧,也许有些朋友不知道。

我们只管创建对象,无须删除对象(当然,对于不要的对象你需要设置一下nil值),Lua会自动删除那些被认为是垃圾的对象。

问题就出现在,什么对象才是垃圾对象,有些时候,我们很清楚某个对象是垃圾,但是,Lua却无法发现。
 
比如这样一个例子:

复制代码 代码如下:

t = {};      -- 使用一个table作为t的key值    key1 = {name = "key1"};    t[key1] = 1;    key1 = nil;      -- 又使用一个table作为t的key值    key2 = {name = "key2"};    t[key2] = 1;    key2 = nil;      -- 强制进行一次垃圾收集    collectgarbage();      for key, value in pairs(t) do        print(key.name .. ":" .. value);    end
首先以一个table,叫做t。

然后创建一个新的table——key1,这个key1作为t的key值,给t新增了一个字段,赋值为1。

同样的,key2也作为t的一个key值。

接着,调用了collectgarbage函数,可以不管它,我们只要知道,它会让lua进行一次垃圾回收。

最后输出t的所有字段,输出结果如下:

复制代码 代码如下:

[LUA-print] key1:1[LUA-print] key2:1


这很符合常理,也在我们的预计当中,虽然我们在给t赋值之后,key1和key2都赋值为nil了。

但是,已经添加到table中的key值是不会因此而被当做垃圾的。

换句话说,key1本身已经是nil值,但它曾经所指向的内容依然存放在t中。key2也是一样的情况。

所以我们最后还是能输出key1和key2的name字段。

2.颠覆你的认知——弱引用table

刚刚举例的只是正常情况,那么,如果我们把某个table作为另一个table的key值后,希望当table设为nil值时,另一个table的那一条字段也被删除。

应该如何实现?

这时候就要用到弱引用table了,弱引用table的实现也是利用了元表。

我们来看看下面的代码,和之前几乎一样,只是加了一句代码:

复制代码 代码如下:
 t = {};      -- 给t设置一个元表,增加__mode元方法,赋值为“k”    <span style="background-color: rgb(255, 255, 0);">setmetatable(t, {__mode = "k"});</span>      -- 使用一个table作为t的key值    key1 = {name = "key1"};    t[key1] = 1;    key1 = nil;      -- 又使用一个table作为t的key值    key2 = {name = "key2"};    t[key2] = 1;    key2 = nil;      -- 强制进行一次垃圾收集    collectgarbage();      for key, value in pairs(t) do        print(key.name .. ":" .. value);    end

留意,在t被创建后,立刻给它设置了元表,元表里有一个__mode字段,赋值为”k”字符串。

如果这个时候大家运行代码,会发现什么都没有输出,因为,t的所有字段都不存在了。

这就是弱引用table的其中一种,给table添加__mode元方法,如果这个元方法的值包含了字符串”k”,就代表这个table的key都是弱引用的。

一旦其他地方对于key值的引用取消了(设置为nil),那么,这个table里的这个字段也会被删除。
 
通俗地说,因为t的key被设置为弱引用,所以,执行t[key1] = 1后,t中确实存在这个字段。

随后,又执行了key1 = nil,此时,除了t本身以外,就没有任何地方对key1保持引用,所以t的key1字段也会被删除。

3.三种形式的弱引用

对于弱引用table,其实有三种形式:

1)key值弱引用,也就是刚刚说到的情况,只要其他地方没有对key值引用,那么,table自身的这个字段也会被删除。设置方法:setmetatable(t, {__mode = “k”});
2)value值弱引用,情况类似,只要其他地方没有对value值引用,那么,table的这个value所在的字段也会被删除。设置方法:setmetatable(t, {__mode = “v”});
3)key和value弱引用,规则一样,但是key和value都同时生效,任意一个起作用时都会导致table的字段被删除。设置方法:setmetatable(t, {__mode = “kv”});
 
当然,这里所说的被删除,是指在Lua执行垃圾回收的时候,并不一定是立刻生效的。我们刚刚只是为了测试,而强制执行了垃圾回收。

[转自]http://www.jb51.net/article/55229.htm


附注:

下面是一个网友检测lua内存泄漏的代码,可以参考一下,转自:Lua内存泄漏应对方法.

    local findedObjMap = nil       function _G.findObject(obj, findDest)          if findDest == nil then              return false          end          if findedObjMap[findDest] ~= nil then              return false          end          findedObjMap[findDest] = true                local destType = type(findDest)          if destType == "table" then              if findDest == _G.CMemoryDebug then                  return false              end              for key, value in pairs(findDest) do                  if key == obj or value == obj then                      _info("Finded Object")                      return true                  end                  if findObject(obj, key) == true then                      _info("table key")                      return true                  end                  if findObject(obj, value) == true then                      _info("key:["..tostring(key).."]")                      return true                  end              end          elseif destType == "function" then              local uvIndex = 1              while true do                  local name, value = debug.getupvalue(findDest, uvIndex)                  if name == nil then                      break                  end                  if findObject(obj, value) == true then                      _info("upvalue name:["..tostring(name).."]")                      return true                  end                  uvIndex = uvIndex + 1              end          end          return false      end            function _G.findObjectInGlobal(obj)          findedObjMap = {}          setmetatable(findedObjMap, {__mode = "k"})          _G.findObject(obj, _G)      end  


思路:

1. 资源跟踪,定位哪些资源泄漏

2. 引用检索,查找泄漏的资源被哪个模块引用

资源跟踪

定义:将应用中分配的lua对象添加到一个弱表中.执行完整的gc后,还能从弱表中索引到的对象表示它还在别的地方被引用着,可能是正常的引用,也可能是一处内存泄漏.我使用了一个弱键表,该表以要跟踪的lua对象为键,该对象的描述信息为值.其中的描述信息包含了对象描述和对象创建时间两项.对象描述用于区别不同的跟踪对象;创建时间则用来在打印弱表的时候判断对象的存活时间是否合理.我定义的接口是:function TraceMem(obj, description);

引用检索

定义:从某个节点开始搜索所有该节点引用的对象以及递归搜索子节点,找到要搜索的对象,打印出引用路径.最常见的可以从_G开始搜索.搜索到的每个table,取其key和value递归搜索;搜索到的每个函数,取其upvalue递归搜索.至于是否需要搜索对象的环境表和metatable,以及全局registry表,则取决于具体需求.我因为用不上,就没有搜索这一部分.搜索的时候注意标记已经搜索过的节点,避免重复搜索.最好能缩小搜索范围,而不是从_G开始搜索,另外应该能每次只搜索指定的部分引用而非全部,可以极大的缩短等待时间.搜索所有的引用其实相当耗时.我定义的搜索接口是:function Search_r(obj, node, mark, result);




0 0
原创粉丝点击