Node.js 的内存限制到底是多少?
熟练掌握 Node.js API 可以让你快速入门,但深刻理解 Node.js 程序的内存占用可以让你走得更远。
让我们首先使用“process.memoryUsage()”来看一下我们的内存使用情况,每秒更新一次:
setInterval(() => { console.log('Memory Usage:', process.memoryUsage()); }, 1000);
由于输出以字节为单位,因此不太方便用户使用。让我们通过将内存使用情况格式化为 **MB** 来对其进行修饰:
function formatMemoryUsageInMB(memUsage) { return { rss: convertToMB(memUsage.rss), heapTotal: convertToMB(memUsage.heapTotal), heapUsed: convertToMB(memUsage.heapUsed), external: convertToMB(memUsage.external) }; } const convertToMB = value => { return (value / 1024 / 1024).toFixed(2) + ' MB'; }; const logInterval = setInterval(() => { const memoryUsageMB = formatMemoryUsageInMB(process.memoryUsage()); console.log(`Memory Usage (MB):`, memoryUsageMB); }, 1000);
现在,我们每秒都可以得到以下输出:
Memory Usage (MB): { rss: '30.96 MB', // The actual OS memory used by the entire program, including code, data, shared libraries, etc. heapTotal: '6.13 MB', // The memory area occupied by JS objects, arrays, etc., dynamically allocated by Node.js // V8 divides the heap into young and old generations for different garbage collection strategies heapUsed: '5.17 MB', external: '0.39 MB' } Memory Usage (MB): { rss: '31.36 MB', heapTotal: '6.13 MB', heapUsed: '5.23 MB', external: '0.41 MB' }
我们都知道V8引擎的内存使用是有限的,不仅受到OS的内存管理和资源分配策略的限制,还受到其自身的设置的限制。
使用 `os.freemem()`,我们可以看到操作系统有多少可用内存,但这并不意味着所有内存都可以被 Node.js 程序使用。
console.log('Free memory:', os.freemem());
对于 64 位系统,Node.js V8 的默认最大旧空间大小约为 1.4GB。这意味着即使您的操作系统有更多可用内存,V8 也不会自动使用超过此限制的内存。
提示:可以通过设置环境变量或在启动 Node.js 时指定参数来更改此限制。例如,如果您希望 V8 使用更大的堆,则可以使用 `--max-old-space-size` 选项:
node --max-old-space-size=4096 your_script.js
这个值需要根据你的实际情况和场景来设置,比如你有一台大内存的机器,单机部署,和你有很多台小内存的机器,分布式部署,这个值的设置肯定会不一样。
让我们进行一个测试,通过无限期地向数组中填充数据直到内存溢出,并查看何时发生这种情况。
const array = []; while (true) { for (let i = 0; i < 100000; i++) { array.push(i); } const memoryUsageMB = formatMemoryUsageInMB(process.memoryUsage()); console.log(`Memory Usage (MB):`, memoryUsageMB); }
这是我们直接运行程序时得到的结果。添加数据一段时间后,程序崩溃了。
Memory Usage (MB): { rss: '2283.64 MB', heapTotal: '2279.48 MB', heapUsed: '2248.73 MB', external: '0.40 MB' } Memory Usage (MB): { rss: '2283.64 MB', heapTotal: '2279.48 MB', heapUsed: '2248.74 MB', external: '0.40 MB' } # # Fatal error in , line 0 # Fatal JavaScript invalid size error 169220804 # # # #FailureMessage Object: 0x7ff7b0ef8070
困惑吗?限制不是 1.4G 吗?为什么会使用超过 2G?实际上,Node.js 的 1.4GB 限制是 V8 引擎的历史限制,适用于早期的 V8 版本和某些配置。在现代 Node.js 和 V8 中,Node.js 会根据系统资源自动调整其内存使用量。在某些情况下,它可能会使用远超过 1.4GB 的内存,尤其是在处理大型数据集或运行内存密集型操作时。
当我们将内存限制设置为 512M 时,rss 达到 996 MB 左右时就会溢出。
Memory Usage (MB): { rss: '996.22 MB', heapTotal: '993.22 MB', heapUsed: '962.08 MB', external: '0.40 MB' } Memory Usage (MB): { rss: '996.23 MB', heapTotal: '993.22 MB', heapUsed: '962.09 MB', external: '0.40 MB' } <--- Last few GCs ---> [22540:0x7fd27684d000] 1680 ms: Mark-sweep 643.0 (674.4) -> 386.8 (419.4) MB, 172.2 / 0.0 ms (average mu = 0.708, current mu = 0.668) allocation failure; scavenge might not succeed [22540:0x7fd27684d000] 2448 ms: Mark-sweep 962.1 (993.2) -> 578.1 (610.7) MB, 240.7 / 0.0 ms (average mu = 0.695, current mu = 0.687) allocation failure; scavenge might not succeed <--- JS stacktrace ---> FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
总结一下,更准确的说,Node.js 的内存限制是指堆内存限制,也就是 V8 分配的 JS 对象、数组等可以占用的最大内存。
堆内存的大小是否决定了 Node.js 进程可以占用多少内存?不!请继续阅读。
我可以将 3GB 的文件放入 Node.js 内存吗?
我们在测试中看到,在程序崩溃之前,数组只能容纳 2GB 多一点。那么,如果我有一个 3GB 的文件,我不能一次性将其全部放入 Node.js 内存中吗?
你可以!
我们通过 process.memoryUsage() 看到了一个外部内存,这个内存是 Node.js 进程占用的,但不是 V8 分配的。只要你把 3GB 的文件放在那里,就没有内存限制。怎么办?你可以使用 Buffer。Buffer 是 Node.js 的一个 C++ 扩展模块,它使用 C++ 来分配内存,而不是 JS 对象和数据。
这是一个演示:
setTimeout(()=>{ let buffer = Buffer.alloc(1024 * 1024 * 3000); }, 3000)
就算你分配3GB的内存,我们的程序依然可以流畅运行,而我们的Node.js程序已经占用了5GB以上的内存,因为这个外部内存并不是Node.js所限制的,而是操作系统对分配给线程的内存的限制(所以你不能肆意妄为,即使是Buffer也会耗尽内存;本质是用Streams来处理大数据)。
在 Node.js 中,Buffer 对象的生命周期与 JavaScript 对象绑定。当 JavaScript 对 Buffer 对象的引用被移除时,V8 垃圾回收器会将该对象标记为可回收,但 Buffer 对象的底层内存并不会立即释放。通常在调用 C++ 扩展的析构函数时(例如在 Node.js 中的垃圾回收过程中),这部分内存会被释放。但这个过程可能并不完全与 V8 的垃圾回收同步。
Memory Usage (MB): { rss: '2392.73 MB', heapTotal: '2392.57 MB', heapUsed: '2359.93 MB', external: '3000.41 MB' } Memory Usage (MB): { rss: '2392.75 MB', heapTotal: '2392.57 MB', heapUsed: '2359.94 MB', external: '3000.41 MB' } Memory Usage (MB): { rss: '2392.75 MB', heapTotal: '2392.57 MB', heapUsed: '2359.94 MB', external: '3000.41 MB' }
总结:Node.js 内存使用由 JS 堆内存使用(由 V8 的垃圾回收决定)+ C++ 的内存分配组成
为什么堆内存分为新生代和老生代?
分代垃圾收集策略在现代编程语言的实现中非常流行!在 Ruby、.NET 和 Java 中可以找到与分代垃圾收集类似的策略。当发生垃圾收集时,它通常会导致“世界停止”的情况,这不可避免地会影响程序性能。然而,这种设计是为了性能优化而构思的。
内存分配发生在 From 中。在垃圾回收期间,From 中的活动对象会被检查并复制到 To,然后释放非活动对象。在后续回收轮次中,To 中的活动对象会被复制到 From,此时 To 会变为 From,反之亦然。每次垃圾回收周期,From 和 To 都会交换。此算法在复制过程中仅复制活动对象,从而避免产生内存碎片。
那么,如何确定变量的活性呢?可达性分析就派上用场了。以以下对象为例:
在可达性分析的背景下:
诚然,引用计数可以作为一种辅助手段,但是在循环引用的情况下,它无法准确判断对象真正的存活情况。
在老生代内存中,对象一般比较不活跃,但是当老生代内存满了之后,就会通过Mark-Sweep算法触发老生代内存的清理(Major GC)。
标记清除算法包括标记和清除两个阶段。标记阶段,V8 引擎会遍历堆中的所有对象,对存活的对象进行标记。清除阶段,只清除未标记的对象。该算法的优点是老生代中死亡对象的比例比较小,因此清除阶段耗时比较短。缺点是只清除不压缩,可能会造成内存空间不连续,不方便为大对象分配内存。
这一缺点导致了内存碎片化,因此需要使用另一种算法——标记-压缩算法。该算法将所有存活对象移至一端,然后一次性清除边界右侧的无效内存空间,从而获得完整连续的可用内存空间。它解决了标记-清除算法可能带来的内存碎片化问题,但代价是移动大量存活对象会花费更多时间。
如果你觉得这篇文章有用,请点赞。:D