基于无阻塞、事件驱动建立的Node服务,具有内存消耗低的优点,非常适合处理海量的网络 请求。
内存控制正是在海量请求和长时间运行的前提下进行探讨 的。在服务器端,资源向来就寸土寸金,要为海量用户服务,就得使一切资源都要高效循环利用。
在Node中通过JavaScript 使用内存只能使用部分内存64位系统下约为1.4 GB,32位系统下约为0.7 GB)
V8的对象分配
在V8中,所有的JavaScript对象都是通过堆来进行分配的。Node提供了V8中内存使用量的查 看方式
process.memoryUsage()
//进程的常驻内存部分
{ rss: 31272960,
//V8的堆内存使用情况,已申请到的堆内存
heapTotal: 9682944,
//V8的堆内存使用情况,当前使用的量
heapUsed: 5400792,
//V8 引擎内部的 C++ 对象占用的内存
external: 16905 }
进程的内存总共有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)中.除了rss外,heapTotal和heapUsed对应的是V8的堆内存信息。heapTotal是堆中总共申请的内
存量,heapUsed表示目前堆中使用中的内存量。这3个值的单位都是字节
Node在启动时可以传递–max-old-space-size或–max-new-space-size来调整内存限制的大小
//用于设置老生代内存空间的最大值
node --max-old-space-size=1700 test.js // 单位为MB
// 或者 设置新生代内存空间的大小的
node --max-new-space-size=1024 test.js // 单位为KB
上述参数在V8初始化时生效,一旦生效就不能再动态改变
。如果遇到Node无法分配足够内
存给JavaScript对象的情况,可以用这个办法来放宽V8默认的内存限制,避免在执行过程中稍微
多用了一些内存就轻易崩溃
v8的垃圾回收机制
V8的垃圾回收策略主要基于分代式垃圾回收机制
在V8中,主要将内存分为新生代和老生代两代。
新生代中的对象为存活时间较短的对象
新生代中的对象主要通过Scavenge算法进行垃圾回收
老生代中的对象为存活时间较长或常驻内存的对象
Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收
高效使用内存
在JavaScript中能形成作用域的有函数调用、with以及全局作用域
闭包是JavaScript的高级特性,利用它可以产生很多巧妙的效果。它的问题在于,一旦有变量 引用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域 中产生的内存占用也不会得到释放。除非不再有引用,才会逐步释放。
在V8中通过delete删除对象的属性有可能干扰V8的优化,所以通过赋值方式解除引用更好
查看内存使用情况
写一个方法不停的分配内存 但不释放 ,并打印日志
// 格式化内存使用情况
var showMem = function () {
var mem = process.memoryUsage();
var format = function (bytes) {
//toFixed 四舍五入number值
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};
console.log('Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) + ' rss ' + format(mem.rss));
console.log('-----------------------------------------------------------');
};
// 不停地分配内存但不释放内存
var useMem = function () {
var size = 20 * 1024 * 1024;
var arr = new Array(size);
for (var i = 0; i < size; i++) {
arr[i] = 0;
}
return arr;
};
var total = [];
for (var j = 0; j < 15; j++) {
showMem();
total.push(useMem());
}
showMem();
以下为输出情况:
可以看到,每次调用useMem都导致了3个值的增长。在接近1500 MB的时候,无法继续分配内
存,然后进程内存溢出了,连循环体都无法执行完成,仅执行了8次。
查看系统的内存占用
os.totalmem() 系统的总内存
os.freemem() 系统的闲置内存
堆外内存
通过process.momoryUsage()的结果可以看到,堆中的内存用量总是小于进程的常驻内存用 量
,这意味着Node中的内存使用并非都是通过V8进行分配的。我们将那些不是通过V8分配的内
存称为堆外内存
用buffer 分配内存并查看内存输出情况
// 格式化内存使用情况
var showMem = function () {
var mem = process.memoryUsage();
var format = function (bytes) {
//toFixed 四舍五入number值
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};
console.log('Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) + ' rss ' + format(mem.rss));
console.log('-----------------------------------------------------------');
};
var useMem = function () {
var size = 200 * 1024 * 1024;
var buffer = new Buffer(size);
for (var i = 0; i < size; i++) {
buffer[i] = 0;
}
return buffer;
};
var total = [];
for (var j = 0; j < 15; j++) {
showMem();
total.push(useMem());
}
showMem();
输出情况如下
我们看到15次循环都完整执行,并且三个内存占用值与前一个示例完全不同。在改造后的输
出结果中,heapTotal与heapUsed的变化极小,唯一变化的是rss的值,并且该值已经远远超过V8
的限制值。这其中的原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制,所以也不 会有堆内存的大小限制。
这意味着利用堆外内存(buffer)可以突破内存限制的问题
Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分
。受V8的垃圾回收限制的主要是V8的堆内存。
内存泄漏
Node对内存泄漏十分敏感,一旦线上应用有成千上万的流量,那怕是一个字节的内存泄漏也
会造成堆积,垃圾回收过程中将会耗费更多时间进行对象扫描,应用响应缓慢,直到进程内存溢
出,应用崩溃。
内存泄
漏通常产生于无意间,较难排查。尽管内存泄漏的情况不尽相同,但其实质只有一个,那就是应 当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象
通常,造成内存泄漏的原因有如下几个
- 缓存
- 队列消费不及时
- 作用域未释放
慎将内存当做缓存
缓存在应用中的作用举足轻重,可以十分有效地节省资源。因为它的访问效率要比I/O的效
率高,一旦命中缓存,就可以节省一次I/O的时间。
但是在Node中,缓存并非物美价廉。一旦一个对象被当做缓存来使用,那就意味着它将会常
驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描
和整理时,对这些对象做无用功。
严格意义的缓存有着完善的过期策略
而普通对象的键值对并没有.
缓存限制策略
// 缓存限制策略 限制缓存对象的大小
var LimitableMap = function (limit) {
this.limit = limit || 10;
this.map = {};
this.keys = [];
};
var hasOwnProperty = Object.prototype.hasOwnProperty;
LimitableMap.prototype.set = function (key, value) {
var map = this.map;
var keys = this.keys;
if (!hasOwnProperty.call(map, key)) {
if (keys.length === this.limit) {
var firstKey = keys.shift();
delete map[firstKey];
}
keys.push(key);
}
map[key] = value;
};
LimitableMap.prototype.get = function (key) {
return this.map[key];
};
module.exports = LimitableMap;
由于模块的缓存机制,模块是常驻老生代的
。在设计模块时,要十分小心内存泄漏的出现。
在下面的代码,每次调用leak()方法时,都导致局部变量leakArray不停增加内存的占用,且不被
释放
var leakArray = [];
exports.leak = function () {
leakArray.push("leak" + Math.random());
};
因为模块都是被包在IIFE中执行的 在加上模块的缓存机制 leakArray 始终常驻在老生代中
如果模块不可避免地需要这么设计,那么请添加清空队列的相应接口,以供调用者释放内存
var leakArray = [];
exports.leak = function () {
leakArray.push("leak" + Math.random());
};
exports.destory = function () {
leakArray.length = 0;
}
缓存的解决方案
直接将内存作为缓存的方案要十分慎重。除了限制缓存的大小外,另外要考虑的事情是,进
程之间无法共享内存。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用
是一种浪费。
使用大量缓存,目前比较好的解决方案是采用进程外的缓存,在Node中主要可以解决以下两个问题:
(1) 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效。
(2) 进程之间可以共享缓存。
市面上较好的缓存有Redis和Memcached
举个实际的例子,有的应用会收集日志。如果欠缺考虑,也许会采用数据库来记录日志。日
志通常会是海量的,数据库构建在文件系统之上,写入效率远远低于文件直接写入
,于是会形成
数据库写入操作的堆积,而JavaScript中相关的作用域也不会得到释放,内存占用不会回落,从而
出现内存泄漏。
在日志收集的案例中,换用文件写入日志的方式会更高效。
需要注意的是,如果生产速度因为某些原因突然激增,或者消费速度因为突然的系统故障降低,内存泄漏还是可能出现的
深度的解决方案应该是监控队列的长度
,一旦堆积,应当通过监控系统产生报警并通知相关
人员.
内存泄漏排查
排查内存泄漏的原因主要通过对堆内存进行分析而找到 node-heapdump和node-memwatch各有所长
大内存应用
Node提供了stream模块用于处理大文件
stream继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。它分可读和可写两种。Node中的大多数模块都有stream的应用,比如fs的createReadStream() createWriteStream()法可以分别用于创建文件的可读流和可写流,process模块中的stdin和stdout则分别是可读流和可写流的示例。
var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);
可读流提供了管道方法pipe(),封装了data事件和写入操作。通过流的方式,上述代码不会
受到V8内存限制的影响,有效地提高了程序的健壮性