node系列--内存控制

520 阅读3分钟

在过去的很长一段时间里,JavaScript开发者很少在开发过程中遇到需要对内存精确控制的场景,也缺乏控制的手段,但是对于一些使用node.js开发的前端工程师会有海量的请求和长时间的运行,如果内存占用太高,而不进行回收的话,进程就会退出。

V8的内存限制

在一般的后端开发语言中,内存使用基本没什么限制,但在node中通过JavaScript只能使用部分内存(64位系统下约为1.4GB,32位系统下约为0.7GB),这种限制就导致了node无法操作大内存对象,所以我们需要知道内存的限制以便更好地进行内存管理。

V8中,所有JavaScript对象都是通过堆来进行分配的,通过process.memoryUsage()就可以查看内存使用量:

$ node
> process.memoryUsage()
{ rss: 14567469,
  heapTotal: 7467903,
  heapUsed: 2728432}

在上述代码中,memoryUsage()返回的3个属性中,rssresident set size缩写,即进程的常驻内存部分,进程的内存总共有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)中,heapTotal是堆中总共申请的内存量,heapUsed表示目前堆中使用中的内存量,这三个值的单位都是字节。 至于v8为何要限制堆的大小,表层原因是v8最初位浏览器而设计,不太可能遇到用大量内存的场景,对于网页来说,v8限制已经基本可以满足了。深层原因是v8的垃圾回收机制的限制,当然这个限制也不是不能打开,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初始化时生效,一旦生效就不能再动态改变。

v8的垃圾回收机制

v8的垃圾回收策略主要是基于分代式垃圾回收机制。但是在实际应用过程中,对象的生存周期长短不一,不同的算法只能针对特定情况具有最好的效果。现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存施以更高效的算法。 主要将内存分为新生代老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。 v8用到的各种垃圾回收算法包括Scavenge算法、Mark-Sweep&Mark-Compact和Incremental Marking

内存泄漏

在v8的垃圾回收机制下,在通常的代码编写中,很少会出现内存泄漏的情况,但是内存泄漏通常产生于无意间,较难排查,那么造成内存泄漏的原因有如下几个。

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

内存泄漏排查

现在已经有许多工具用于定位Node应用的内存泄漏。下面是一些常见的工具。

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

这里主要介绍node-heapdumpnode-memwatch两种方式进行内存泄漏的排查。

node-heapdump

首先先构造一份包含内存泄漏的代码将其存为server.js文件:

var leakArray = []
var leak = function () {
    leakArray.push("leak"+Math.random())
};
http.createServer(function(req,res){
    leak();
    res.writeHead(200,{'Content-Type':'text/plain'});
    res.end('hello world\n')
}).listen(3000)
  • 安装node-heapdump
$ npm install heapdump

安装完后,在代码的第一行添加如下代码将其引入:

var heapdump = require('heapdump')

引入node-heapdump后,就可以启动服务进程,并接受客户端的请求,访问多次之后leakArray中就会具备大量的元素,此时我们通过向服务进程发送SIGUSR2信号,让node-heapdump抓拍一份堆内存的快照,发送信号命令如下:

$ kill -USR2 <pid>

这份抓取的快照将会在文件目录下以heapdump-<sec>.<usec>.heapsnapshot的格式存放。

node-memwatch

node-memwatch的用法和node-heapdump一样,我们需要准备一份具有内存泄漏的代码。这里不再赘述node-memwatch的安装过程,整个示例代码如下:

var memwatch = require('memwatch');
memwatch.on('leak',function(info){
    console.log('leak:');
    console.log(info);
});

memwatch.on('stats',function(stats){
    console.log('stats:');
    console.log(stats);
});

var http = require('http');

var leakArray = [];
var leak = function(){
    leakArray.push("leak"+Math.random())
}
http.createServer(function(req,res){
    leak();
    res.writeHead(200,{'Content-Type':'text/plain'});
    res.end('hello world\n')
}).listen(3000)
console.log('服务运行在http://127.0.0.1:3000')

大内存应用

在Node中,不可避免地还是会存在操作大文件的场景,由于Node的内存限制,操作大文件需小心,好在Node提供了stream模块用于处理大文件。 stream模块是Node的原生模块。直接引用即可。stream继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。分为读和写两种,Node中的大多数模块都有stream的应用,比如fs的createReadStream()createWriteStream()方法可以分别用于创建文件的可读流和可写流,process模块中的stdinstdout则分别是可读流和可写流的示例。 由于v8的内存限制,我们无法通过fs.readFile()fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()fs.createWriteStream()方法通过流的方式实现对大文件的操作。下面的代码展示了文件读写的过程:

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

由于读写模型固定,上述方法有更简洁的方式,示例代码如下:

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

可读流提供了管道方法pipe(),封装了data事件和写入操作。通过流的方式,上述代码不会受到v8内存限制的影响,有效提高了程序的健壮性。