node中的内存控制

基于无阻塞、事件驱动建立的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内存限制的影响,有效地提高了程序的健壮性

发布了77 篇原创文章 · 获赞 7 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_37653449/article/details/89790018