Node.js内存控制

服务器端性能敏感,内存管理的好坏很关键。

V8的垃圾回收机制

V8的内存限制

单个Node进程只能使用部分内存。

64位系统约1.4G,32位系统约0.7G,这导致Node不能直接操作大内存对象。

原因:

  • V8引擎主要是服务于浏览器端的

  • 垃圾回收机制的限制

V8的对象分配

查看内存使用量:

1
process.memoryUsage();

返回值

1
2
3
4
5
{
rss: 133454,
heapTotal: 71000,
heapUsed: 33200
}

堆内存是动态变化的,不足时会继续申请,直到超过V8限制。

V8的限制是可以放宽的,在Node启动时传递参数:

1
2
3
4
// 单位是MB
node --max-old-space-size=1700 test.js
// 单位是KB
node --max-new-space-size=1024 test.js

V8的垃圾回收

垃圾回收策略:分代式垃圾回收机制。

没有一种算法可以胜任所有场景,按对象的存活时间分代,匹配不同的回收算法。

内存分代:

  • 新生代:存活时间短的对象,Scavenge回收算法

  • 老生代:存活时间长

Scavenge算法

以空间换时间的算法

  • 把堆内存分为两块,一个处于使用状态,一个处于闲置状态,

  • 分配对象时,会放入使用空间中

  • 当垃圾回收时,只把存活的对象复制到闲置区,清空活动区

  • 此时,活跃区和闲置区互换

  • 存在晋升的情况:即把对象移动到老生代内存中

Mark-Sweep & Mark-Compact算法

Mark-Sweep:

  • 标记阶段:遍历堆中的对象,标记活的对象

  • 清除阶段:清除没有被标记的对象

Mark-Sweep会导致内存空间会出现不连续状态。

Mark-Compact:基于Mark-Sweep

  • 标记阶段:同上

  • 整理阶段:标记后,把活的对象移动到内存的一端

  • 清除阶段:清除边界外的内存

Mark-Compact需要移动对象,效率较低。

V8主要使用Mark-Sweep,内存不足时才使用Mark-Compact。

增量标记(Incremental Marking)

全停顿:垃圾回收时会暂停JS执行

全停顿时间过长后导致卡顿。

V8做了大量优化:

  • 增量标记:把一次停顿拆分成多次执行。

查看垃圾回收日志

垃圾回收日志:启动参数--trace_gc

gc.log

性能分析日志:--prof

v8.log

Node提供了工具统计日志信息:tick-processor

高效使用内存

作用域

JS中能形成作用域的有函数、with和全局作用域。

标识符查找:沿着作用域链向上查找。

全局作用域直到进程退出才会释放,所以全局变量引用的对象会常驻在老生代内存中,需要主动释放:

  1. delete

  2. 给全局变量重新赋值

闭包

作用域链上的对象访问只能向上,外部不能访问内部作用域。

实现外部访问内部变量的方法叫做“闭包”,高阶函数特性。

闭包的实现:函数的返回值是一个匿名函数,匿名函数可以访问函数的内部变量,外部函数通过这个匿名函数就可以访问到内部函数的变量。

闭包会导致匿名函数及其原始函数作用域得不到释放。

内存指标

查看内存使用情况

查看进程内存情况

1
process.memoryUsage()

rss:resident set size,驻留集合大小,进程的常驻内存部分

查看系统内存情况

1
2
3
4
// 系统全部内存
os.totalmem()
// 系统闲置内存
os.freemem()

堆外内存

进程的堆内存总是小于rss,这说明Node中的内存使用并非都是V8进行分配的。

不是通过V8分配的内存——称为“堆外内存”

  • V8的堆内存

  • Node的堆外内存

Buffer对象不经过V8的内存分配,也不会有堆内存的大小限制。这是因为,Node需要处理网络流和文件IO流,浏览器端一般不需要。

内存泄漏

内存泄漏的本质是:应当回收的对象出现意外没有被回收,常驻在老生代内存中。

造成内存泄漏的原因:

  • 缓存

  • 队列消费不及时

  • 作用域未释放

缓存

慎将内存当做缓存,把一个对象当做缓存,会让该对象常驻老生代内存,如果不限定大小、没有过期策略会导致内存占用无限增长。

服务端程序是一个长时间运行程序,许多在浏览器、App等短时间运行程序中的可容忍的不良行为会持续放大,最终导致程序崩溃。

缓存限制策略

LRU算法

模块是常驻老生代内存的,设计时需要小心内存泄漏

进程外内存

使用专门的缓存软件实现缓存,如Radis、Memcached,好处:

  • 减少常驻内存的对象数量

  • 可以跨进程共享缓存

队列问题

队列在消费者-生产者模型中充当中间产物,一般情况下,消费速度远大于生产速度,不会出现内存泄漏,但一旦消费速度低于生产速度,就会形成堆积。

数据库的写入效率低于文件直接写入

解决方案:

  • 采用消费速度更快的技术

  • 监控队列长度报警

  • 给调用提供超时机制和拒绝机制

内存泄漏排查

常见工具:

  • node-heapdump

  • node-mtrace

  • node-memwatch

大内存应用

存在操作大文件的场景

(1)通过流的方式读写大文件:stream模块

(2)如果不进行字符层面的操作,可使用纯粹的Buffer操作