前言
我写的一段 Node 程序在连续运行12个小时后后出现了崩溃的情况,怀疑是内存溢出导致的,于是我来验证一下,然后看看怎么修复。
正文-排查思路
查看内存限制
Node.js 是基于 v8 引擎运行的, 而 v8 是chorme 浏览器的核心,操作系统不可能让浏览器占用无限制的资源,因为这样会挤占其他应用程序的空间,所以给 chrome 分配的内存是有限制的,同样的,在 Node.js 端运行的 v8 也是如此,那么首先要查看当前设备在运行 Node.js 程序时,内存限制是多少。
打开终端进入 Node 环境,执行 v8.getHeapStatistics(),查看 v8 堆内存信息,查询最大堆内存 heap_size_limit,当然这里包含了新、老生代、大对象空间等。我的电脑硬件内存是 16G,Node版本 16.17.0,查到的 heap_size_limit 是 4G。
验证
知道了内存限制,现在我要给程序中加一段代码,打印出来实时的内存占用情况,以确认就是这个内存溢出最后超出 4G 大小限制导致的。
// 通过 process 定期读取内存使用情况
const memoryUsage = process.memoryUsage()
const rss = (memoryUsage.rss / 1024 / 1024).toFixed(2)
const heapUsed = (memoryUsage.heapUsed / 1024 / 1024).toFixed(2)
console.log(`内存占用: RSS ${rss} MB, 堆使用: ${heapUsed} MB`)
上面代码中的 process 对象是 Node.js 中的一个全局对象,用于提供与当前 Node.js 进程相关的信息和控制。它是 Node.js 核心的一部分,无需 require 就可以在任何地方直接访问;它包含了许多有用的方法和属性,可以帮助你与操作系统交互、管理内存、处理环境变量等。
我们利用它计算出当前进程的内存占用情况,其中 rss 是操作系统分配给整个进程的总内存,包括堆、栈和 V8 之外的内存使用(例如 Buffer)。
heapUsed 是 JavaScript 堆中实际使用的内存大小, 堆内存是 V8 引擎用来存储 JavaScript 对象、变量和执行代码的地方。
其中我们需要关注的是 heapUsed,当它超出限制时,程序将中断,报错:JavaScript heap out of memory
通过打印这个内存信息,我发现,确实当程序执行12小时左右,异常中断报错后,实时的内存占用确实达到了 4G。
修复
Node.js 语言其实是有自动的垃圾回收机制的(GC),就是为了防止内存泄漏,只不过我写的程序由于涉及大量计算和对象的频繁创建销毁,导致垃圾回收机制没有及时触发,才导致了内存溢出;
既然自动的垃圾回收在此场景下不好用,那么我们可以手动安排垃圾回收的时机:
// 该操作是为了防止内存激增,给垃圾回收留时间
await new Promise((resolve) => setImmediate(resolve));
只需要通过 setImmediate 手动增加一个异步延迟,通过使用 setImmediate,你将当前操作推到下一个宏任务队列中,使 Node.js 有机会在这段代码执行之前去处理其他任务(包括垃圾回收)。在这一瞬间,JavaScript 引擎可以检查内存是否有机会被释放掉,从而优化内存的使用。
额外说一句,setImmediate 和 requestAnimationFrame,是不同环境下替代 setTimeout 的最佳选择,分别优化了异步代码的执行和动画的流畅性。
总结
当程序长时间运行后出现异常中断,最常见的原因就是内存溢出了,因为只要出现了溢出,超出内存限制就是时间问题。