Node.js-内存控制

V8的垃圾回收机制与内存限制

JavaScript与Java一样,采用垃圾回收机制进行自动内存管理。

V8的内存限制

Node基于V8,因此受到V8的一些限制:只能使用部分内存(64位系统下约1.4GB,32位系统下约为0.7GB)。在单个Node进程的情况下,计算机的内存资源无法得到充足的使用。

在V8中,所有JavaScript对象都是通过来进行分配的。查看内存信息:

$ node
> process.memoryUsage();
{ rss: 26107904,
  heapTotal: 6733824,
  heapUsed: 4678648,
  external: 9288 }

其中:heapTotal是申请到的堆内存,heapUsed是当前堆内存使用量。

堆分配过程: 声明变量并赋值 --> 所使用对象的内存分配在堆中 --> 如果申请到的堆空闲内存不够则继续申请,直到堆的大小超过V8的限制为止

要调整堆内存限制的大小,在Node启动时传入参数--max-old-space-size或者--max-new-space-size:

node --max-old-space-size=1700 test.js //单位为MB
// or
node --max-new-space-size=1024 test.js //单位为KB

V8的垃圾回收机制

V8主要的垃圾回收算法

按对象的存活时间将内存分为新生代(new space)和老生代(old space):

  • 新生代(new space):存放存活时间较短的对象。采用Scavenge算法
  • 老生代(old space):存放存活时间较长或常驻内存的对象。采用Mark-Sweep算法Mark-Compact算法

垃圾回收算法需要应用逻辑暂停,待回收后再恢复执行,即“全停顿”(stop-the-world)。为了降低全堆垃圾回收带来的停顿时间,V8在标记阶段采用增量标记(incremental marking)将垃圾回收拆分成许多小“步进”,每完成一次“步进”后执行应用逻辑一小会儿,再执行“步进”,依次交替重复直到标记阶段完成。
还引入了延迟清理(lazy sweeping)增量式整理(incremental compaction)等方式提高性能。

可以总结出:
V8堆 = 新生代(new space) + 老生代(old space)
新生代(new space) = semi space(From) + semi space(To)

--- new space old space V8堆大小
--- semi space * 2 --- ---
64位系统 32MB * 2 1400MB 1464MB
32位系统 16MB * 2 700MB 732MB

查看垃圾回收日志

在启动时添加--trace_gc参数,可以得到垃圾回收执行情况:

node --trace_gc test.js > gc.log

在启动时添加--prof参数,可以得到V8执行时的性能分析数据:

node --prof test.js

执行命令之后,会在该目录下产生一个 *-v8.log 的日志文件,我们可以安装一个日志分析工具 tick:

$ npm install tick -g
$ node-tick-processor *-v8.log
[Top down (heavy) profile]:
  Note: callees occupying less than 0.1% are not shown.
  inclusive      self           name
  ticks   total  ticks   total
    426   36.7%      0    0.0%  Function: ~<anonymous> node.js:27:10
    426   36.7%      0    0.0%    LazyCompile: ~startup node.js:30:19
    410   35.3%      0    0.0%      LazyCompile: ~Module.runMain module.js:499:26
    409   35.2%      0    0.0%        LazyCompile: Module._load module.js:273:24
    407   35.1%      0    0.0%          LazyCompile: ~Module.load module.js:345:33
    406   35.0%      0    0.0%            LazyCompile: ~Module._extensions..js module.js:476:37
    405   34.9%      0    0.0%              LazyCompile: ~Module._compile module.js:378:37

也可以使用 headdump 之类的工具将日志导出,然后放到 Chrome 的 Profile 中去分析。

Node中的内存

在正常的JavaScript执行中,无法立即回收的内存有闭包和全局变量引用这两种情况。此类变量会导致老生代中的对象增多,由于V8的内存限制,因此要注意此类变量是否无限制的增加。

Node的内存由V8堆内存和Node分配的堆外内存构成。V8的垃圾回收限制的主要是的V8的堆内存。Node自行分配的堆外内存不受此限制。

内存泄漏

通常,造成内存泄漏的原因有:

  • 缓存
  • 队列消费不及时
  • 作用域未释放

缓存导致内存泄露的解决办法

  1. 需要限制缓存的无限增长,限制缓存的数量,一旦超过限制,则按一定规则清楚缓存。
  2. 如果业务要求不能预设缓存大小,请添加清空队列之类的释放内存的接口。
  3. 大量缓存尽量采用进程外的缓存,例如Redis或Memcached等。

队列消费不及时导致内存泄露的解决办法

  1. 更换消费速度更高的技术,设定消费速度的下限值(例如设定超时异常)。
  2. 监控队列的长度,队列堆积时提供报警并通知相关人员。
  3. 当队列过长时,队列不接受新数据并响应队列已满的错误。

内存泄漏的排查

使用工具,例如:node-heapdump,node-memwatch等工具来分析堆内存,来排查内存泄漏的原因。

大内存应用

如果不可避免要操作大文件,请使用Node提供的stream,buffer等模块来处理。
使用时仍然要注意物理内存的限制。

JSRUN前端笔记, 是针对前端工程师开放的一个笔记分享平台,是前端工程师记录重点、分享经验的一个笔记本。JSRUN前端采用的 MarkDown 语法 (极客专用语法), 这里只属于前端工程师。