引用计数 gc 机制使用不当导致内存泄漏
上一篇文章找同事 review 了一下,收到的反馈是铺垫太长了,我尽量直入正题,哈哈
最近 dbd 压测时发现内存泄漏,其实这个问题去年已经暴露了,参见这篇 博客【压测周】。当时排查不够仔细,在此检讨下。关于 dbd 的内存问题,还有 这篇博客 讨论线程安全,以及 这篇博客 讨论临时变量的处理。当时还存了一个尾巴,因为用到关联数组进行脏数据管理,这部分数据是怎么释放的一直没搞明白。今天算是搞明白了,这个内存泄漏的 bug 也源于此。
基于引用计数的 gc 机制,应该是在引用计数为 0 的时候释放内存的,lpc 也基本没有例外。具体详情可以参看 free_svalue 的实现,里面基本是引用计数减去 1,然后检查是否引用计数为 0,若是 0 就执行相关的 Free 操作。其中,有一半的篇幅都是讲字符串的释放的,剩下的则是 object、buffer、array、mapping、class 以及 function 的释放问题。所以,基本上,lpc 不需要像 golang 一样,停止一切操作做 mark and swap 的 gc 操作。也不同于 Lua 的标记清除,不需要分步执行,只要引用计数为 0,立马释放掉。
为什么是基本上呢?因为对于 object 类型,lpc 并没有对其立即释放,而是放到一个链表里面,待主循环里没有函数执行时,再行销毁。这样做,一方面是为了配合复杂的热更新机制,另一方面,则是解决 object 循环引用的需要。对于对象的销毁,首先调用的是 destruct_object, 这个函数最终的作用是为对象打上销毁标志。在这个函数里,针对 master object 和 simul_efun object 做了热更新的处理。然后从栈上移除 object 的所有引用,从 object#n 列表里移除这个 object。最后打上销毁标记,并放入待销毁列表。
下一步,则调用 destruct2, 首先将该 object 的所有变量释放。如果该对象有对自身引用,则因为这一步而引用计数减一,方便接下来释放该对象本身。接着调用 free_object, 对象引用次数为 0,已打上销毁标记,功成身退,成功析构。
由于在 dbd 中,没有跑 lpc 脚本,所有的关联数组(mapping)都是在 C 层生成和使用的,因此引用计数需要手工维护,这就是万恶之源了。对于脚本层,新建的变量,引用计数都应该为 1,退出变量作用域时,则引用计数减一。放入 mapping 时,引用次数相应加 1。而在 C 层使用 mapping 时,所有变量的组织都是围绕 mapping 展开的,变量的生命周期就等同于 mapping 的生命周期。当序列化数据后,释放数据对应的 mapping 时,会对其中的所有变量引用计数 -1,但依然计数不为 0,因此无法释放。
解决的办法也相当简单,变量放入 mapping 前,先 free_svalue,将引用计数变为 0,则加入 mapping 后,引用计数为 1,生命周期就同 mapping 一样了。