众所周知,JavaScript 使用基于标记-清除算法的自动垃圾回收机制,一般来说在使用闭包、定时器等功能时注意释放就不会有内存泄漏问题。但有些时候,内存泄漏也会出现在引擎盖下。
有一个很正常的需求,读入一个文件,然后按行分割对每一行进行处理。
Minyami 的直播下载模式就是定时读取服务器的 m3u8 文件,经过 m3u8 parser,找出需要下载的 chunk,这个过程中就对 m3u8 文件进行了按行分割。
用户反馈,在下载 YouTube DVR 直播时发生内存泄漏,实测可复现,漏的还很多,如果挂个一两个小时肯定是要 OOM 的。
按照排查内存泄漏的基本操作,启用 node remote debugger,用 Chrome 的调试工具对内存抓取 snapshot,打几个点进行 diff 。
结果出现了类似图中的情况(图是随便截的,不是真实情况)。sliced string 中的 parent 常常为整个 m3u8 的内容,而对于 YouTube 来说每个 m3u8 可能有数 MB,这就造成了非常快的内存泄漏。
一开始我以为是我的定时器——被用在控制任务超时——发生了泄漏,于是我手动在处理任务的时候加上了 finally 然后把定时器释放掉,结果发现并无效果。
最后搜索了一番
https://github.com/nodejs/help/issues/711
发现 v8 内部,对 split/slice 出的字符串会在内部保留一个对 parent 的引用,因为 split 出的字符串一直被使用,这个 parent 也就不会被释放,当有数千个 split 出的字符串时,可能就会保留数百个对不同的 parent 的引用。实际上在我们的应用场景中,parent(m3u8 内容)只需要解析过之后就可以扔掉了,这里的引用是无用而浪费的。
对 split 出的字符串做了一次复制,使 parent 不再 active,自动被 gc,问题解决。
See:
https://github.com/Last-Order/Minyami/blob/master/src/core/m3u8.ts#L43