简述Node.js内存限制

前段时间在Ghost博客系统群与群友交流,谈到了Node内存管理的问题,发现很多小伙伴对Node内存管理完全处于一个“零认识”的状态,这可能与本身工作没有接触到相关,毕竟我们大多数工作可能与DOM打交道,当页面内存占用过多(可能性也很低),几乎不用等GC,用户已经刷新或关闭了网页。

做为前端工程师的我们,大多使用Node做一做小工具,这大多是短时间执行的场景,即使内存使用过多或是内存泄漏,也只会影响到我们使用者,随着进程的退出,内存也会释放。但面对Node服务端应用的开发,内存管理这样一个话题就不可避免的摆在了我们面前。虽然我对Node的认知也比较浅薄,但觉得也有必要将知道的内存相关知识整理一下。

第一次看重内存管理是在做推妹子前端占位图的时候,顺便了解了一下相关知识,算下来正好快一年了。基于Node无阻塞、事件驱动建立的网络服务,本身就具有内存消耗低的优点,但面对海量的图片处理请求,也不得不让我考虑超低配云服务器内存的问题。

V8内存管理

在浏览器开发工作中,我几乎没有遇到过垃圾回收对程序构成的性能影响。知道的也就是老生常谈的通过eval('Code')或是setTimeout('Code', 0)等方法动态改变作用域带来的性能影响。但都不会这样去写代码。我们知道Node的JavaScript执行引擎是V8,关于V8的历史,或是关于Node的历史我们就不在这里赘述了,毕竟学一门语言和使用一门框架,最先应该了解的是它是基于怎样的场景为了解决什么样的问题而诞生的。但我们必须要记住和了解的是:Node对JavaScript执行的优异表现直接受益于V8。它可以随着V8的升级享受到更好的性能表现和实现新的语言特性。但也会因为V8受到一些限制。

V8内存限制

关于Node的内存限制,正如上文所说,Node可以随着V8的升级享受更好的性能,但也会因为V8收到一些限制。所以,关于Node的内存限制,也正是V8所带来的。==比较有趣的是,V8其中一个限制是针对于内存的,V8允许操作部分内存,在64位系统下约为1.4GB,32位系统下约为0.7GB。==这就导致了Node无法直接操作大的内存对象,即使物理内存再大也没有用。同时这样也出现了一个比较尴尬的问题,那就是:计算机的内存资源无法得到充分的利用。

之所以会产生内存被限制的情况,是因为V8的是针对浏览器的,所以V8这套内存管理机制在该场景下使用起来绰绰有余,完全可以胜任浏览器运行JS的各种情况。但也是正因如此,限制了Node开发者使用大内存的想法。

那在开发中我们怎么查看内存的使用情况呢?我们可以使用process.memoryUsage()方法。那么这个方法会返回一个对象:

  • rss: Resident Set Size的简写,及进程常驻内存
  • heapTotal: 堆中申请的内存总大小
  • heapUsed: 实际使用的堆的内存大小

以下是文档对返回参数的解释:

heapTotal and heapUsed refer to V8's memory usage. external refers to the memory usage of C++ objects bound to JavaScript objects managed by V8. rss, Resident Set Size, is the amount of space occupied in the main memory device (that is a subset of the total allocated memory) for the process, which includes the heap, code segment and stack.

我们随便运行一下可能是这种情况(下面运行环境全在Windows 10 64位系统下):

{ 
    rss: 23359488,
    heapTotal: 7159808,
    heapUsed: 4414576,
    external: 8224 
}

上面看到,我们使用process.memoryUsage()命令后,返回的结果可能会出现一个external参数。这个参数表示使用的【堆外内存】。那什么是堆外内存呢?等会我们从一个实例中解答。先说说内存限制的事,我们看到heapTotal目前只有7159808Bytes,是远远低于1.4G的限制的,这就说明V8对内存的分配并不是一次性给予,而是用多少就申请多少(总是大于使用量)。也就是说,在我们进行变量赋值时,所使用对象的内存就分配在堆中。如果申请的堆空间不足以分配新的对象,将继续申请新的堆内存,直到超过V8的限制。

我们就用下面代码看看:

// 保存生成的数组
let total = [];

const showMemory = function () {
  // 获取当前内存使用情况
  let mem = process.memoryUsage();

  // 格式化内存方法
  function format (bytes) {
    return (bytes / 1024 / 1024).toFixed(2) + 'MB'
  }

  // 输出内存使用情况
  console.log(`申请的内存heapTotal:${format(mem.heapTotal)}  使用中的内存heapUsed:${format(mem.heapUsed)}  进程常驻内存rss:${format(mem.rss)} `)

  console.log('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^')
};

// 生成一个大数组
const useMemory = function () {
  let size = 2 * 1024 * 1024;
  let arr = new Array(size);
  for (let i = 0; i < size; i++) {
    arr[i] = 1;
  }
  return arr
};


for (let i = 0; i < 100; i++) {
  showMemory();
  total.push(useMemory())
}

输出的结果:

申请的内存heapTotal:6.83MB  使用中的内存heapUsed:4.21MB  进程常驻内存rss:22.36MB
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
申请的内存heapTotal:22.84MB  使用中的内存heapUsed:20.25MB  进程常驻内存rss:39.54MB
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

// 省略中间若干输出

申请的内存heapTotal:1383.84MB  使用中的内存heapUsed:1380.51MB  进程常驻内存rss:1400.96MB
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
申请的内存heapTotal:1399.85MB  使用中的内存heapUsed:1396.51MB  进程常驻内存rss:1416.98MB
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

<--- Last few GCs --->

[15040:000001911F1FFF60]     1583 ms: Mark-sweep 1396.0 (1403.8) -> 1395.9 (1400.8) MB, 140.5 / 0.0 ms  last resort GC in old space requested
[15040:000001911F1FFF60]     1723 ms: Mark-sweep 1395.9 (1400.8) -> 1395.9 (1400.8) MB, 139.8 / 0.0 ms  last resort GC in old space requested

// 省略后面错误...

很明显,heapTotal达到接近1.4G时显示了内存溢出。那么V8为什么要做这样的限制呢?那还是得从V8是为浏览器设计的JS引擎说起,上面我们也提到了在浏览器上,几乎不可能出现使用大量内存的场景,但仅仅是这个原因吗?深层次的原因在于V8的垃圾回收机制的限制。根据官方的说法,以1.5G的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1s以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。这种情况无论是前端后端都无法接受的。所以在这种情况下,限制内存也是个不错的选择。

当然,这些限制也是可以调整的,在启动Node程序时传递--max-old-space-size参数,但这个修改其实也是杯水车薪,只能说稍微可以多用一点内存。

node --max-old-space-size=1900 app.js // 单位MB
// 或者
node --max-old-space-size=1024 app.js // 单位MB

上述参数在V8初始化时生效,一旦生效就不能动态的改变。

堆外内存

在上面我们提到,使用process.memoryUsage()方法,可能会出现一个external参数,那这个参数指定的时堆外内存大小,那什么时堆外内存呢?

我们可以从上面的所有输出结果看到,堆中内存用量总是小于进程的常驻内存用量,这意味着Node中的内存使用并非都是通过V8进行分配的。我们将那些不是通过V8分配的内存称为【堆外内存】。