关于 Lua 内存泄漏的检测

  前一阵开始和同事一起优化内存,首先是优化 Lua 内存,因为发现每次战斗完后 Lua 内存非常大,从 3M 左右在经过了10次左右的战斗后,会暴增到近 100M,很明显是有内存泄漏。
     然后我正式启动该工作,基本思路就是递归遍历内存中所有的数据,表,函数,协程,用户数据,查看未释放和笔误引起的全局变量泄漏;于是通过搜索我参考了以下资料:
   Lua 官方手册(最重要)
 
     以上资料有各自的参考价值,但是也有些不正确和不符合我要求的地方,一是搜索的根节点不是从 _G 开始,而是从 debug.getregistry 开始,否则你会遗漏很多数据;二是我不想用 c 写,而是直接用 lua 实现,把结果打印到 txt 里。
     首先我的搜索方式如下以递归的方式进行:
 
 
  • 对搜索到的每一个数据进行引用计数并放置在 weak table 中。
  • 查找全局变量泄漏:启动游戏打印一份完整的游戏数据,游戏退出前打印一份完整的内存数据,然后把差异的部分再过滤输出并且按照引用次数进行排序,然后逐个查找所有可疑或者不该出现的全局变量(一般都在根节点),直接定位修改代码,直到没有全局变量泄漏位置。
  • 查找游戏逻辑数据未释放:比如查找战斗逻辑泄漏,在每次进入主场景打印一份完整的数据,这样每次战斗完成都会回到主场景,而且理论上回到主场景战斗数据都是必须释放的,然后对比最近两次主场景中打印的内存数据,将差异部分输出并且按照引用次数排序,然后根据结果优化或者修改代码逻辑,将没有释放的地方进行释放。
  • 不断地循环以上方式,直到内存稳定且总量在合理预期范围内。
     通过以上方式,解决了项目中的 Lua 内存泄漏,长时间连续游戏,Lua 内存稳定在 3.5M 左右,高峰时 会到 5M。
 

  
  2017-05-05 更新:
  (如果你发现它对你有所帮助,请贡献一个友好的 Star 吧:)
 
  应有些朋友的要求建立一个QQ群,大家可以自行交流工具使用和其它技术,所以我建了一个,QQ群号:330366204。
 

  
  2017-07-01 更新
  多说两句关于 Lua 内存泄露,之前这个项目出现如此离谱的情况是因为全局变量的问题,这个工具对于查找全局变量的内存泄露很有效。所以对于 Lua 5.1,应该主动对所有的逻辑脚本封装沙盒,Lua 5.3 的 _ENV 有效的改善了这个问题,但是也应该进一步封装逻辑脚本的沙盒,这样逻辑开发人员再怎么写全局变量也不会出问题,唯一容易内存泄露的地方就是大量引用了 C# 端的游戏对象和各种脚本,以此链接了大量的对象和资源等等再经历了很多游戏场景后依然无法得到有效的释放。
 

  
  2017-07-21 更新
  很开心这个工具分享后帮助一些朋友解决了问题,但是在跟一些朋友沟通中发现有些重要的接口方法没有用上,或者工具使用不正确,为此我彻底更新了 GitHub 页面上的使用说明(英文)。在这里再简单说下工具使用。
  首先 require 这个脚本,例如:local mri = require(MemoryReferenceInfo)
  然后在某个地方打印一份内存快照:
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshot("./", "All", -1)

  快照文件的内容,每一行是一个引用对象信息,所有的信息按照引用次数降序排列,每一行被 tab 分成了3列,分别是:对象类型/地址,引用链,引用次数。整个文件可以使用 Excel 打开,会自动归为3列,方便阅读,重新排序。

  文件内容中重点部分是引用链的信息,例如 "function: 0x7f85f8e0e3f0 registry.2[_G].Author.Ask[line:33@file:example.lua] 1" 这条信息说明的是:表 "registry" 的成员 "2"(也就是表 "_G")引用了表 "Author",表 "Author" 有一个成员 "Ask" 引用了 "function: 0x7f85f8e0e3f0",函数位置在文件 "example.lua" 中的第33行,一共被引用了1次。这样就能快速的定位什么对象在哪里被引用,一共被引用了多少次。

  "DumpMemorySnapshot" 这个方法最后两个参数是“根节点对象名称“和“搜索根节点对象”,默认值为 "registry" 和 "debug.getregistry()",在大多数使用的时候不需要修改使用默认值即可,但是当你想从别的根节点开始搜索来缩小范围,例如从 "_G" 来搜索,你可以手动设置这两个参数,例如:

-- Only dump memory snapshot searched from "_G".
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshot("./", "All", -1, "_G", _G)

  当整个程序运行一段时间后,再打印一份内存快照(可以打印多份),接下来最重要的工作就是对比快照分析增加的泄露点。在这个工具中,提供了一个名为 “DumpMemorySnapshotComparedFile” 的接口来实现这个对比功能,切记不要自己用文件对比工具来对比两份快照(有朋友这样用过),因为快照内容是根据引用计数来降序排序的,时间不同内容也不同,顺序也不同,所以普通的文件对比工具在这里是无法生效的。使用方法:

mri.m_cMethods.DumpMemorySnapshotComparedFile("./", "Compared", -1, 
"./LuaMemRefInfo-All-[1-Before].txt", 
"./LuaMemRefInfo-All-[2-After].txt")

这个方法会生成一个新文件,里面是出现在第二份快照里但是没有并出现在第一份快照里的数据,这就是新增内容。

  无论是那种类型的数据,如果 dump 后数据过大,但是想查看某个特定的数据,可以使用过滤器来生成一个新文件,可以选择新文件生成的内容是包含关键字,还是排除关键字,例如:

-- 输出文件里所有包含关键字 “Author” 的内容。(不区分大小写)
mri.m_cBases.OutputFilteredResult("./LuaMemRefInfo-All-[2-After].txt", "Author", true, true)

--输出文件里所有不包含关键字 “Author” 的内容。(不区分大小写)
-- Filter all result exclude keywords: "Author".
mri.m_cBases.OutputFilteredResult("./LuaMemRefInfo-All-[2-After].txt", "Author", false, true)

  另外,如果想查看某个对象到底被哪些地方引用着,可以使用接口 "DumpMemorySnapshotSingleObject",例如:

--输出所有引用对象 "_G.Author" 的地方。 
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshotSingleObject("./", "SingleObjRef-Object", -1, "Author", _G.Author)

-- 输出所有引用字符串 "yaukeywang" 的地方。
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshotSingleObject("./", "SingleObjRef-String", -1, "Author Name", "yaukeywang")

  通过以上几个主要的方法配合使用,就可以快速的查出内存泄漏,即使在手机上也可以使用,比如打印时将保存路径指向 sd 卡目录,例如如果使用 Unity 里的 Lua,可以使用:

collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshot(UnityEngine.Application.persistentPath, "All", -1)

它将输出一份快照文件到 sd 卡目录下。

  现在新加了一个配置选项,一般例如 "DumpMemorySnapshot" 这个方法都是指定一个保存路径和额外信息,然后保存的文件名最后每次都会加上当前的时间戳,方便根据时间来区分不同的快照,也避免需要频繁的设置和修改文件名,也避免同一个地方不同时间的快照被不断覆盖,这个时间戳选项默认开启,可以通过 "mri.m_cConfig.m_bAllMemoryRefFileAddTime = false" 来关闭,配置的设置放置在 "require" 后,Dump 之前,其它几个 Dump 的接口也都有是否附加时间戳到文件名的选项,具体参看源码。

  除了以上的方法,还提供了一些其它的接口可以使用,更详细的使用请参考 GitHub 上的 ReadMe 和源码中的接口定义说明,都写的很详细了,"Example.lua" 中也演示了常用接口的使用方法。

  最后,最近完善了下这个工具,增加了字符串类型的输出,所以上面的那张搜索路径图,路径上可以再添加一个 "string"。同时需要注意:为了能在同一行显示所有字符串(以方便其他方法对数据进行处理,例如对比差异增量,Excel 排序统计等),字符串在显示的时候所有的回车和换行符:'\r', '\n' 都被显示的替换成了 '\\n',需要阅读数据的时候注意。

posted @ 2016-04-01 16:35  yaukey  阅读(25903)  评论(22编辑  收藏  举报