关于 Lua 内存泄漏的检测

摘要:
不久前,我开始和同事们一起优化记忆。第一个是优化Lua内存。我发现每次战斗后,Lua的内存都很大。经过大约10场战斗后,它将从大约3M激增到接近100M,这显然是内存泄漏。通过以上方法,我们解决了项目中的Lua内存泄漏问题。我们连续玩了很长时间的游戏。Lua内存稳定在3.5M左右,在高峰时间将达到5M。2017-07-01更新详细介绍了Lua内存泄漏。由于全球变量的问题,之前的项目是如此荒谬。该工具对于查找全局变量的内存泄漏非常有效。
前一阵开始和同事一起优化内存,首先是优化 Lua 内存,因为发现每次战斗完后 Lua 内存非常大,从 3M 左右在经过了10次左右的战斗后,会暴增到近 100M,很明显是有内存泄漏。
然后我正式启动该工作,基本思路就是递归遍历内存中所有的数据,表,函数,协程,用户数据,查看未释放和笔误引起的全局变量泄漏;于是通过搜索我参考了以下资料:
http://shavingha.blog.163.com/blog/static/10378336200822134554488/
http://blog.csdn.net/shimazhuge/article/details/43794347
http://blog.codingnow.com/2012/12/lua_snapshot.html
https://github.com/cloudwu/lua-snapshot/blob/master/snapshot.c
http://stackoverflow.com/questions/11366693/lua-count-the-no-of-references-to-a-table
Lua 官方手册(最重要)
以上资料有各自的参考价值,但是也有些不正确和不符合我要求的地方,一是搜索的根节点不是从 _G 开始,而是从debug.getregistry 开始,否则你会遗漏很多数据;二是我不想用 c 写,而是直接用 lua 实现,把结果打印到 txt 里。
首先我的搜索方式如下以递归的方式进行:
关于 Lua 内存泄漏的检测第1张
  • 对搜索到的每一个数据进行引用计数并放置在 weak table 中。
  • 查找全局变量泄漏:启动游戏打印一份完整的游戏数据,游戏退出前打印一份完整的内存数据,然后把差异的部分再过滤输出并且按照引用次数进行排序,然后逐个查找所有可疑或者不该出现的全局变量(一般都在根节点),直接定位修改代码,直到没有全局变量泄漏位置。
  • 查找游戏逻辑数据未释放:比如查找战斗逻辑泄漏,在每次进入主场景打印一份完整的数据,这样每次战斗完成都会回到主场景,而且理论上回到主场景战斗数据都是必须释放的,然后对比最近两次主场景中打印的内存数据,将差异部分输出并且按照引用次数排序,然后根据结果优化或者修改代码逻辑,将没有释放的地方进行释放。
  • 不断地循环以上方式,直到内存稳定且总量在合理预期范围内。
通过以上方式,解决了项目中的 Lua 内存泄漏,长时间连续游戏,Lua 内存稳定在 3.5M 左右,高峰时 会到 5M。

2017-05-05 更新:
源码地址:https://github.com/yaukeywang/LuaMemorySnapshotDump
(如果你发现它对你有所帮助,请贡献一个友好的 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 排序统计等),字符串在显示的时候所有的回车和换行符:' ', ' ' 都被显示的替换成了 '\n',需要阅读数据的时候注意。

免责声明:文章转载自《关于 Lua 内存泄漏的检测》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇jupyter之配置自己喜欢的python环境免杀工具汇总下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

AIX 5L 系统管理技术 —— 存储管理——卷组

卷组 在安装系统时,就会创建一个rootvg卷组。包含自带硬盘(内置硬盘)和系统逻辑卷,一个系统只能有一个rootvg卷组。一般情况下rootvg卷组最好只包含自带硬盘。 一、创建卷组 在创建卷组之前,系统管理员必须知道如下所列的信息。 (1)卷组的名字在系统中必须是唯一的。卷组名要求时一个字符串,长度时1至15字符。 (2)要确定新卷组中包含哪些物理卷,...

Lua初学习 9-14_03 迭代器 ( ipairs 与 pairs)

前言 :所谓迭代器:就是一种可以遍历一种集合中所有元素的机制         Lua中,通常将迭代器表示为函数,每一次调用函数,即返回集合中下一个元素 1:ipairs 与 pairs 的区别 t1 = {"one","two","three","four"}  --数组 ==== ipairs遍历 ===== for k,v in ipairs(t1)...

ZFS文件系统及Freenas介绍

一、简介   1、什么是zfs文件系统     ZFS文件系统的英文名称为Zettabyte File System,也叫动态文件系统(Dynamic File System),是第一个128位文件系统。最初是由Sun公司为Solaris 10操作系统开发的文件系统。作为OpenSolaris开源计划的一部分,ZFS于2005年11月发布,被Sun称为是终...

Lua function 函数

Lua支持面向对象,操作符为冒号‘:’。o:foo(x) <==> o.foo(o, x). Lua程序可以调用C语言或者Lua实现的函数。Lua基础库中的所有函数都是用C实现的。但这些细节对于lua程序员是透明的。调用一个用C实现的函数,和调用一个用Lua实现的函数,二者没有任何区别。 函数的参数跟局部变量一样,用传入的实参来初始化,多余的实...

[转]天龙八部服务器端Lua脚本系统

一、Lua脚本功能接口 1. LuaInterface.h/.cpp声明和实现LuaInterface。 LuaInterface成员如下: //脚本引擎 FoxLuaScriptmLua ; //注册器 LuaCFuncRegistermFuncRegister; //场景关联 Scene*mOwner; //已经读取的脚本表 IDTablem_Sc...

[Unity热更新]tolua# &amp;amp; LuaFramework(一):基础

一、tolua# c#调用lua:LuaState[变量名/函数名] 1.LuaState a.执行lua代码段 DoString(string) DoFile(.lua文件名) Require(.lua文件名(但没有.lua后缀)) b.获取lua函数或者表 LuaFunction func = lua.GetFunction(函数名);      ...