[TOC]
问题1:Node如何利用CPU和I/O这两个服务器资源?
问题2:Node如何合理高效地是用内存?
一切都与Node的JavaScript执行引擎V8息息相关
- 64位系统下约为1.4GB
- 32位系统下约为0.7GB
为什么V8要限制堆的大小呢?
- 表层原因因为V8最初为浏览器而设计,不大可能遇到大量是用内存的场景。对于网页来说,V8的限制值已经足以应付。
- 深层的原因是V8的垃圾回收机制的限制。按照官方的说法,以1.5GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至需要1秒以上。这是垃圾回收中引起的JavaScript线程暂停执行的时间,在这样的时间开销下,应用的性能和响应能力会直线下降。这样的情况不仅后端服务器无法接受,前端浏览器也无法接收。
在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
--
新生代中主要通过Scavenge
算法进行垃圾回收。
Scavenge
算法实现中主要采用了Cheney
算法。
Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为
semispace
。在这两个semispace
空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace
空间称为From
空间,处于闲置状态的称为To
空间。当我们分配对象时,先是在From
空间中进行分配。当开始进行垃圾回收时,会检查From
空间中存活的对象,这些存活的对象将被复制到To
空间中,而非存活对象占用的空间将会被释放。完成复制后,From
空间和To
空间进行互换。
Scavenge
缺点是只能使用堆内存的一般,这是由划分空间和复制机制所决定的。但是Scavenge只复制存活的对象,并且对于声明周期短的场景存活对象占很少部分,所以他在时间效率上有优异的表现,它是典型的牺牲空间换取时间的算法。所以在新生代中尤为合适。
当一个对象经过多次复制依然存户时,它将被认为是生命周期较长的对象。较长生命周期的对象随后会被移到老生代中,采用新的算法进行管理,这一过程称为晋升
。
晋升条件主要有两个:
- 是否经历过
Scavenge
回收 To
空间的内存占用比超过限制
Mark-Sweep: 标记清除算法,它分为标记和清除两个阶段。
但是Mark-Sweep
会有一个最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态,碎片化。这时如果分配一个大对象时可能所有的碎片空间无法完成,就会提前触发垃圾回收机制,而这次回收是不必要的。
所以为了解决这个问题,结合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
与作用域相关的即是标志符查找。所谓标志符,可以理解为变量名。
一直沿着作用域链查找到全局作用域,最后抛出未定义错误。
如果变量是全局变量(不通过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中主要可以解决一下问题:
- 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效
- 进程之间可以共享缓存
目前推荐: Redis
、Memcached
例如:
- 消费速度 > 生产速度 = 正常
- 消费速度 < 生产速度 = 不正常
在队列实现时,优化代码的同时也要考虑到监控队列的长度,一旦消费速度小于生产速度,堆积对象过多时,应当通过系统产生报警并通知;或者任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个限定值。
常见工具
- v8-profiler
- node-heapdump
- node-mtrace
- dtrace
- node-memwatch
一般通过对堆内存进行分析而找到泄露原因。