服务器与大带宽专家 · 持牌IDC/CDN/ISP服务商
简米科技官网JIANMI TECH
资讯 2026-11-09 简米科技 3,771 字 9 分钟阅读

Lua虚拟机清空如何防内存泄漏和性能损耗,关闭时内存泄漏怎么解决?

导读清空lua虚拟机时,避免内存泄漏和性能损耗的关键在于:先断开所有引用,再按引用层次反向释放,最后才调用垃圾收集器收尾,顺序对了,内存不残留,停顿也短;顺序错了,collectgarbage调十遍也无济于事,lua虚拟机内存泄漏怎么办:清空前的三件事很多朋友一提到清空lua虚拟机,第一反应就是执行collectg……

清空lua虚拟机时,避免内存泄漏和性能损耗的关键在于:先断开所有引用,再按引用层次反向释放,最后才调用垃圾收集器收尾,顺序对了,内存不残留,停顿也短;顺序错了,collectgarbage调十遍也无济于事。

lua虚拟机内存泄漏怎么办:清空前的三件事

很多朋友一提到清空lua虚拟机,第一反应就是执行collectgarbage,但业内专家认可的做法恰恰相反:先别急着让gc干活,而是先把所有能触达的内存引用都剪断,gc只能回收没有根引用的对象,只要还有一根线牵着,那块内存就会永远站在那。

具体到lua_State里,有三处地方最容易藏着“线”:

  • lua栈上残留的数据,用lua_settop(L, 0)可以快速清空当前栈,但如果你在C函数里压入了临时值,忘记弹出,那这些值就会在栈底默默生根。
  • 注册表(registry)里的自定义引用,很多C扩展会把指针或table塞进注册表,用luaL_ref拿个整型句柄,清空时若只清全局表,注册表里的引用还活着,对象就不会被回收。
  • 协程(thread)和upvalue,未关闭的协程持有自己的栈和函数环境;闭包引用的upvalue里可能挂着一个巨大的table或userdata,这些引用不切断,gc就永远看不到你的“清空”。

第一步:把lua栈退干净

先执行lua_settop(L, 0),这条命令会丢弃当前栈上所有元素,但注意,这只是在lua_State层面的操作,如果某些元素被注册表或全局表引用了,栈退干净只是表层动作。

随后检查栈顶状态:lua_gettop(L)应该返回0,若还有残留,说明之前有lua_pushxxx和lua_pop不配对,这是最常见的编码疏漏,却往往在清空时才暴露。

第二步:清空注册表里的引用

注册表本质上是一个全局table,位置在LUA_REGISTRYINDEX,清空时不能直接置nil,因为很多内部机制依赖于它,正确做法是遍历注册表,只清理你手动添加的键。

常见手动添加的位置:

  • 通过luaL_ref得到的整数键,会存在注册表里,键是整数。
  • 通过lua_pushstring(L, "my_ptr")后lua_settable存入的自定义字符串键。

遍历注册表时,从1开始到luaL_len结束,遇到你认识的引用就置nil,一个建议是清空优先级:先清table和userdata,再清闭包,因为这些对象内部可能互相引用。

第三步:关闭协程,释放upvalue

遍历注册表时,你会遇到类型为LUA_TTHREAD的协程对象,不要犹豫,直接lua_close掉整个协程,或者用lua_resetthread重置它,协程栈里的局部变量和调用链上的upvalue都会随之释放。

游走的闭包也要处理,如果一个闭包被全局变量或注册表引用,它的upvalue就活着,清空时,先删除所有引用了闭包的全局变量,再迭代注册表,确保没有闭包残留。

lua虚拟机的性能开销在哪里:清理时的停顿与损耗

清空操作对性能的影响,主要来自两个地方:gc的扫描步长和频繁的lua_gc调用。

很多人在循环中反复调用lua_gc(L, LUA_GCCOLLECT, 0),以为能立刻释放内存,实际上这样反而会让gc花大量时间做全量标记,而且在循环里每次都会触发步进,导致严重的停顿。

临时停掉gc,等清空完再收一次

更聪明的做法是:清空前先执行lua_gc(L, LUA_GCSTOP, 0)暂停自动gc,然后做上述的引用剪断工作,此时所有对象都处于不可达状态,但不会立刻被回收,等你把该清的引用都清完,再执行lua_gc(L, LUA_GCRESTART, 0)恢复gc,并调用一次lua_gc(L, LUA_GCCOLLECT, 0)做完整回收。

这个过程对性能的损耗可以压到最低:

  • 避免了清理过程中gc反复步进的额外开销。
  • 一次性标记并回收,比分次做更高效。
  • 不会出现清到一半gc介入导致当前被清对象重新挂到某个临时引用上的情况。

大块对象挨个置nil比整体引爆更平滑

如果虚拟机里存着几个几十MB的大table,直接用lua_pushnil连续lua_next清理,比把根table置nil再gc要平稳得多,因为单个大table在gc标记阶段需要遍历所有键值,若根table直接不可达,gc会用一个极长的原子步进去标记清理,卡顿明显,分批次逐层置nil,每次gc的步进相对均匀。

lua虚拟机性能优化方法:清空顺序决定速度

清空lua虚拟机不是无脑置nil,而是有优先级和层次顺序的,行业共识认为,按照引用层次反向释放是性能最优路径。

先释放深层对象,再释放根引用

比如你要清空一个名为game_data的全局table,它内部关联了地图表、怪物表、掉落表,如果直接game_data = nil,那这三个子表会在gc时被一次性遍历,但如果先遍历game_data,把每个子表先置nil,让子表对象的引用先断开,最后再把game_data本身置nil,gc的负担就会分散成多次小步进。

具体操作:

  • 用lua_getglobal取到game_data。
  • 用lua_pushnil配合lua_next迭代其中的键值对,将值为table的字段先置nil。
  • 重复这个动作,直到game_data里没有table类型。
  • 最后将全局变量game_data置nil。

对于深层的嵌套结构,可以递归清理,但要注意,递归深度过大时会站c栈,建议控制深度或转为迭代。

复用lua_State还是直接关闭重建?

这是一个选择题,频繁重建虚拟机对象,即lua_newstate和lua_close,性能损耗较大,因为每次创建都要初始化全局状态、字符表、gc根节点,如果业务场景是“热更新”,我个人倾向于复用同一个lua_State,只做深度清理。

但如果name虚拟机中嵌入的C库绑定了很多全局对象,而且这些对象自身存在复杂的析构逻辑,深度清理反而容易出错,此时干脆lua_close整个虚拟机,再创建一个新的,虽然耗时稍高,但胜在干净,热更新场景下,多数团队选择直接重建,因为重复的lua_State实例已经足够常见,虚拟机的创建成本在几十毫秒到百毫秒量级,完全可接受。

实战场景:游戏热更新中如何干净重置lua环境

在游戏项目里,热更新时清理lua虚拟机是最容易踩坑的场景,我见过一个实际案例,某团队在重置时只执行了lua_settop(L,0)和collectgarbage,结果下次加载新代码时,旧模块的全局表现时隐时现,后来定位到原因:某个C模块把lua函数指针存在了注册表里,一直没清掉。

正确的热更新清理步骤如下:

  • 保存必要状态:把要跨版本保留的数据,比如玩家配置、等级信息,先复制到C侧或临时表里。
  • 暂停gc,执行lua_settop(L,0)。
  • 遍历注册表,移除所有C模块挂在注册表上的引用,但保留LUA_RIDX_MAINTHREAD等lua内部索引。
  • 遍历全局环境表LUA_RIDX_GLOBALS,把非保留项逐一置nil。
  • 关闭所有未完成的协程,用lua_resetthread重置到初始状态。
  • 恢复gc,执行一次全量收集。
  • 重新加载业务脚本,并把之前保存的状态注入回新的全局环境。

这种顺序下,内存泄漏的点基本都被堵住,性能损耗也集中在最后一次gc上,不会出现反复卡顿。

如果你在嵌入式设备上跑lua,比如arm开发板,内存本来就不充裕,这时候更要把lua虚拟机内存泄漏怎么办这个问题前置,在写C扩展时就养成用luaL_ref后必定配对luaL_unref的习惯,别等到清空时才补救。

Q&A:lua虚拟机清空常见问题解答

问:调用了collectgarbage后内存还是不降,为什么?

答:因为gc只能回收不可达对象,如果某个table还被注册表或全局变量引用,即使你肉眼看不到它,它也不会被回收,优先检查注册表,特别是通过luaL_ref生成的整数键,以及C侧保存的指向lua对象的指针,C指针持有lua对象但不让lua知道,是导致内存不降的最大元凶。

问:lua和c++之间传对象时,如何避免清空虚拟机时泄漏?

答:规则很简单:谁持有,谁负责,C++侧用std::shared_ptr持有lua对象的引用时,必须为lua注册一个对应的析构回调,在回收c++对象时同时执行luaL_unref或lua_pushnil清掉lua侧的引用,清空虚拟机的顺序上,先执行C++对象析构,再清lua全局表,最后gc,才能避免c++对象持有的lua引用变成悬空指针。

问:频繁重置lua虚拟机和使用整体重建哪个更划算?

答:取决于虚拟机的复杂度,如果你的虚拟机只加载了几个简单脚本,整体重建更干净且核心理念简单,如果虚拟机绑定了大量C扩展,每个扩展初始化都要打开依赖库,那复用现有lua_State做深度清理更划算,虽然写清理代码要费点心思,但运行时的损耗远小于反复lua_newstate和lua_close的代价。

分享本文
本文为 简米科技官网 原创,已由运维技术专家审核。转载请注明来源:原文链接
售前咨询 服务热线 售后 邮箱