现在的位置: 首页 > 综合 > 正文

[Lua]Lua内存泄露检测原理

2019年01月08日 ⁄ 综合 ⁄ 共 3526字 ⁄ 字号 评论关闭

lua内存泄露

首先第一点,lua中的内存泄露和我们所说的c/c++中的内存泄露本质上是不一样的。

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

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

检测原理

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

在脚本中:

  • 运行的lua脚本本身就是lua_state
  • _G就是_G全局表。
  • Registry表可以用debug.getregistry获取。
  • global_mt可以用debug.getmetatable获取。 

所以我们就可以在脚本层次实现内存泄露的检测模块。

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

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

搜索流程图(_G表)

 

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

补充:

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

所以我们可以用另外一种实现:就是把用户自己创建的资源对象统统都丢到weak表中,运行完程序后强制GC,然后去查看weak表,若表中还保存着那个对象,就意味着这个对象还有外部引用(相对弱引用我们就叫它为强引用吧),资源没有被GC掉,所以我们可以说这个对象很有可能是内存泄露了。 

Lua垃圾回收算法 

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

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

  • white:白色表示没有进行过标记的节点
  • gray:灰色表示已经进行过标记的节点,但是与它相关联的节点还没有进行过标记。
  • black:本节点和与之关联的节点都已经被扫描标记过了。

通常会出现有关联数据的,包括有Tableupvalue等数据类型。 

垃圾收集器函数

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

"stop"

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

"restart"

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

"collect"

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

"count"

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

"step"

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

"setpause"

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

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

"setstepmul"

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

控制了收集器的工作速度,这个速度是一个相对于内存分配的速度。更大的数字将导致收集器工作的更主动的同时,也使每步收集的尺寸增加。 小于 的值会使收集器工作的非常慢,可能导致收集器永远都结束不了当前周期。 缺省值为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 KB
end

function collect1()
    print("now,Lua内存为:",collectgarbage("count"))-->608.060546875 KB
    collectgarbage()
    collectgarbage()
    print("now,Lua内存为:",collectgarbage("count"))-->204.8408203125 KB
    --最后与一开始只差只有1KB
end
function 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;这些增加的内存,由于已放到了全局函数中,是永远没有机会被回收到了!
end

function 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周期,但是它会降低程序的内存消耗总量,并可能因此减少分页。只有谨慎地测试才能给你最佳的参数值。

抱歉!评论已关闭.