内存控制

问题1:Node如何利用CPU和I/O这两个服务器资源?

问题2:Node如何合理高效地是用内存?

一切都与Node的JavaScript执行引擎V8息息相关

V8的内存限制

  • 64位系统下约为1.4GB
  • 32位系统下约为0.7GB

为什么V8要限制堆的大小呢?

  • 表层原因因为V8最初为浏览器而设计,不大可能遇到大量是用内存的场景。对于网页来说,V8的限制值已经足以应付。
  • 深层的原因是V8的垃圾回收机制的限制。按照官方的说法,以1.5GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至需要1秒以上。这是垃圾回收中引起的JavaScript线程暂停执行的时间,在这样的时间开销下,应用的性能和响应能力会直线下降。这样的情况不仅后端服务器无法接受,前端浏览器也无法接收。

V8的垃圾回收机制

内存分代回收算法

在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

分代

新生代:Scavenge算法

新生代中主要通过Scavenge算法进行垃圾回收。

Scavenge算法实现中主要采用了Cheney算法。

Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为semispace。在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的称为To空间。当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中存活的对象,这些存活的对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间进行互换。

Scavenge缺点是只能使用堆内存的一般,这是由划分空间和复制机制所决定的。但是Scavenge只复制存活的对象,并且对于声明周期短的场景存活对象占很少部分,所以他在时间效率上有优异的表现,它是典型的牺牲空间换取时间的算法。所以在新生代中尤为合适。

新生代堆内存


当一个对象经过多次复制依然存户时,它将被认为是生命周期较长的对象。较长生命周期的对象随后会被移到老生代中,采用新的算法进行管理,这一过程称为晋升

晋升条件主要有两个:

  • 是否经历过Scavenge回收
  • To空间的内存占用比超过限制

老生代: Mark-Sweep & Mark-Compact算法

Mark-Sweep: 标记清除算法,它分为标记和清除两个阶段。

Mark-Sweep


但是Mark-Sweep会有一个最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态,碎片化。这时如果分配一个大对象时可能所有的碎片空间无法完成,就会提前触发垃圾回收机制,而这次回收是不必要的。

所以为了解决这个问题,结合Mark-Compact,标记整理对象标记死亡后的碎片化空间。

Mark-Compact


三种垃圾回收算法简单对比

回收算法 Mark-Sweep Mark-Compact Scavenge
速度 中等 最慢 最快
空间开销 少(有碎片) 少(有碎片) 双倍空间(无碎片)
是否移动对象 科目标签设置

查看垃圾回收日志

启动时添加trace_gc参数,在进行垃圾回收时,将会从标准输出中打印垃圾回收日志

node --trace_gc -e "var a = []; for(var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log

高效使用内存

作用域

1.标志符查找

与作用域相关的即是标志符查找。所谓标志符,可以理解为变量名。

2.作用域链

一直沿着作用域链查找到全局作用域,最后抛出未定义错误。

3.变量的主动释放

如果变量是全局变量(不通过var声明或定义在global变量上),由于全局作用域需要直到进程退出才能释放,此时将导致引用的对象常驻内存(常驻在老生代中)。如果需要释放常驻内存对象,可以通过delete操作来删除引用关系。或者将变量重新赋值,让旧的对象脱离引用关系。

虽然delete操作和重新赋值具有相同的效果,但是在V8中通过delete删除对象的属性有可能干扰V8的优化,所以通过赋值方式解除引用更好。

闭包

实现外部作用域访问内部作用域中变量的方法叫做闭包。

闭包是JavaScript的高级特性,在使用内存时,它的问题在于:一旦有变量引用了这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域产生的内存占用也不会得到释放。除非不再引用,才会逐步释放。


内存指标

内存使用情况

  • 查看进程内存占用 process.memoryUsage()
  • os模块查看操作系统内存使用情况 totalmem() freemem()

堆外内存

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

Node的内存构成主要通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。


内存泄露

通常造成内存泄露的原因:

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

JavaScript创建一个缓存对象

var cache = {}
var get = function(key) {
    if(cache[key]) {
        return cache[key]
    } else {
        // get from ohterwise
    }
}

var set = function(key, value) {
    cache[key] = value
}

慎将内存做缓存

缓存限制策略:限制键值数量,大小

目前比较好的解决方案是采用进程外的缓存,进程本身不存储状态。外部的缓存软件有着良好的缓存过期淘汰策略以及自身的内存管理,不影响Node进程的性能。

在Node中主要可以解决一下问题:

  • 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效
  • 进程之间可以共享缓存

目前推荐: RedisMemcached

关注队列状态

例如:

  • 消费速度 > 生产速度 = 正常
  • 消费速度 < 生产速度 = 不正常

在队列实现时,优化代码的同时也要考虑到监控队列的长度,一旦消费速度小于生产速度,堆积对象过多时,应当通过系统产生报警并通知;或者任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个限定值。

内存排查

常见工具

  • v8-profiler
  • node-heapdump
  • node-mtrace
  • dtrace
  • node-memwatch

一般通过对堆内存进行分析而找到泄露原因。