Node——内存控制

JS在浏览器中运行的时候并不存在太大的内存问题,我们通常也不刻意的去优化他们,但是当运行在服务器端的时候,运行时间长,这种问题就不得不考虑了。

V8的垃圾回收机制与内存限制

V8的内存限制

在64位下只能使用1.4GB,在32位下0.7GB。即便你的物理内存有32GB,单个Node进程也只能使用这些内存。如果你要将一个2G的文件读到内存里解析,good luck。
V8之所以要限制内存的大小,是因为V8垃圾回收的限制。以1.5G的垃圾回收堆内存为例,V8做一次小的垃圾回收要50毫秒以上,做一次非增量式的垃圾回收要1秒以上,这是在回收过程中JS线程被暂停的时间,这是不可接受的。所以目前比较好的办法就是限制住使用的内存。
这个限制也不是不能打破,你可以选择在启动时修改它:

node --max-old-space-size=1700 test.js // 单位为MB 
node --max-new-space-size=1024 test.js // 单位为KB

这个只在初始化时生效,一旦生效就不能动态改变了。
新版本node的限制貌似取消了,至少我的机器到6个G时才报错的。

V8的垃圾回收机制

V8的垃圾回收机制主要基于分代式垃圾回收机制,将内存分为新生代和老生代两代,老生代是存活时间较长或常驻内存的对象,新生代为存活时间较短的。刚才那两个命令就分别是对这两个的设置。老生代的限制为1400/700MB,新生代的是32/16MB。
Scavenge算法
这个算法主要用在新生代内存区域中,因为这个算法的主要思想是牺牲空间来换取时间的。
算法将新生代内存分为相等的两份,一个使用,一个闲置。
处于使用状态的空间成为From空间,闲置的称为To空间,当我们分配对象时,是在From空间中进行分配的。
当垃圾回收开始时,会检查From空间中的存活对象,将这些存活对象复制到To空间中,非存活的对象在这个过程中就被释放掉了。复制完成后,To和From空间互换。
可以看到,它很快,但是费空间,不过对于新生代这种少量的内存来说是很划算的。
在单纯的Scavenge算法中,所有的存活对象都会被复制到To空间,但是在分代垃圾回收的大背景下,有些存活对象会被复制到老生代内存中。
当这个对象已经经历过一次Scavenge回收,它会被复制到老生代;当这个To空间已经使用了超过25%时,会被复制到老生代。因为To会在复制完成后变为From,新的内存分配在这里产生,它必须有足够的空余空间。
**Mark-Sweep & Mark-Compact **
在老生代中使用上面的算法显然是不可能的。
这里首先使用Mark-Sweep。这是标记清除法。它遍历堆中的所有对象,并标记活着的,在清除阶段中清除所有未被标记的对象。
在新生代中,只复制活的,在老生代中,只清理死的。这两个都分别是两部分中较少的那部分,所以这一整套垃圾回收比较高效。
在使用Mark-Sweep进行清除后,内存变得不连续了,这对接下来的内存分配会有影响,还会提前触发下一次垃圾回收。所以有了Mark-Compact,它将活着的对象往前移来填补空白。Mark-Compact过程是很慢的,V8只在空间不足分配新来的新生代时使用。
**Incremental Marking **
因为垃圾回收涉及对程序对象的删除,肯定需要将程序逻辑停下来,对于新生代来说不是什么问题,但是老生代就会很慢,于是有了增量标记,也就是垃圾回收与应用逻辑交替进行。
同样的还会有增量式整理和延迟清理。

高效使用内存

作用域

在某个局部作用域中的对象会随着局部作用域的销毁而被释放,在下次垃圾回收的时候就会清理掉这部分内存,如果全局作用域中的对象过多,那么这些对象存在的作用域直到继承退出才会被释放,这些对象也会最终停留在老生代内存区域中。
如果你想手动释放一个变量,可以使用delete操作符,但是并不推荐这样做,这样做会干扰V8引擎的优化,推荐使用将对象赋值为null或undefined来手动释放它。

闭包

闭包的使用使得JS有了许多优秀的特性,但是这样也带来了问题,一个闭包被赋值给一个变量以后,这个闭包所在的作用域也就不会被销毁,这个作用域中对象所使用的内存也不会被释放,这个要小心一下。

内存指标

进程的内存占用

使用process.memoryUsage()可以看到内存的使用情况。它返回的对象有3个属性rss:进程的常驻内存部分;,heapTotal是堆中总共申请的内存量;heapUsed表示目前堆中使用中的内存量。
我们可以测试一下:

 var showMem = function () {   
    var mem = process.memoryUsage();   
    var format = function (bytes) {     
        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 < 150; j++) {   
    showMem();   
    total.push(useMem()); 
} 
showMem();

这个方法会不断的分配内存但不释放,到最后:

Process: heapTotal 6086.95 MB heapUsed 6083.24 MB rss 6099.39 MB ---------------------------------------------------------------- 
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory

这里可以看到,在所有的rss中,堆内存占了大部分。

系统内存的占用

使用os模块中的函数来查看机器的物理内存及其使用情况:

var os = require("os");
console.log(os.totalmem());
console.log(os.freemem());

堆外内存

从上面的结果中我们可以看到,堆内存的总量总是小于rss。
我们将前面的useMem方法稍微改造一下,每一次构造一个200M的对象:

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; 
};
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 3012.91 MB

可以看到,这里buffer并未被分派到堆内存中,Buffer对象不同于其他对象,它不经过V8的内存分配机制,所以也不会有堆内存的大小限制。
这意味着利用堆外内存可以突破内存限制的问题。

内存泄露

内存泄露在前端页面上问题不太大,但是在服务器端就是个不得不考虑的问题。造成这个问题的原因有:

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

慎将内存用作缓存

缓存是很有效的节省IO的办法,但是在Node中,一旦一个对象被当做缓存来使用的时候就要格外的小心了,这意味着它将常驻在老生代内存中,这样的缓存越大意味着垃圾回收在做越多的无用功。
所以创建一个有完善过期机制的缓存来控制缓存的增长是很有必要的。
可以通过限制键的数量等方法来控制缓存的增长。
还有一个通常会被我们忽略的问题,就是模块的缓存由于模块的缓存机制,它是常驻老生代的。我们通过exports导出的函数是可以访问文件模块中的私有变量的,这样每个文件模块在编译执行后形成的作用域由于模块缓存的原因不会被释放,所以设计模块时要十分小心内存泄露。这里举个例子:

var leakArray = []; 
exports.leak = function () {   
    leakArray.push("leak" + Math.random()); 
};

这里每次调用leak方法,都会导致局部变量leakArray不停的增加内存的占用。
且进程间无法共享内存,在进程内使用缓存会造成进程间缓存无法共享,这对内存是一种浪费。如果需要大量缓存,最好使用进程外缓存比如Redis和Memcached。

关注队列状态

这也是一个不经意产生的内存泄露。队列一般在消费者-生产者模型中充当中间人的角色,当消费大于生产时没有问题,但是当生产大于消费时,会产生堆积,就容易发生内存泄露。
比如收集日志,如果日志产生的速度大于文件写入的速度,就容易产生内存泄露,表层的解决办法是换用消费速度更高的技术,但是这不治本。根本的解决方案应该是监控队列的长度一旦堆积就报警或拒绝新的请求,还有一种是所有的异步调用都有超时回调,一旦达到时间调用未得到结果就报警。

内存泄露排查

node-heapdump
node-memwatch
这两个模块可以用来检测内存泄露,它们可以通过事件和抓取内存快照的方式来为我们分析哪里有内存泄露提供依据。

大内存应用

不可避免的我们会遇到大文件操作的问题。由于Node内存的限制,操作大内存时要小心。stream模块为我们提供了支持,这是一个原生模块。

var fs = require("fs");
var reader = fs.createReadStream('in.txt'); 
var writer = fs.createWriteStream('out.txt'); 
reader.on('data', function (chunk) {   
    writer.write(chunk); 
    console.log(chunk);
}); 
reader.on('end', function () {  
    writer.end(); 
});

由于读写模式固定,专门提供了一个pipe方法:

var reader = fs.createReadStream('in.txt'); 
var writer = fs.createWriteStream('out.txt'); 
reader.pipe(writer);

如果并不是字符串层面的操作,则可以使用纯粹的Buffer来操作。

原文链接:https://www.jianshu.com/p/1bd61fc742b0

发表评论

登录后才能评论