Node.js内存泄漏分析

chenzj0705 8年前

来自: http://blog.csdn.net/dan_blog/article/details/50755117


在极客教育出版了一个视频是关于《Node.js 内存泄漏分析》,本文章主要是从内容上介绍如何来处理Node.js内存异常问题。如果希望学习可前往极客学院:
本文章的关键词
- 内存泄漏
- 内存泄漏检测
- GC分析
- memwatch


文章概要

由于内存泄漏在Node.js中非常的常见,可能在浏览器中应用javascript时,对于其内存泄漏不是特别敏感,但作为服务器语言运行时,你就不得不去考虑这些问题。由于很小的逻辑可能导致服务器运行一天或者一个星期甚至一个月才会让你发现内存不断上涨,而终于会到那天你不得不重启服务来保护服务器的性能,那么这种问题就有必要在上线前进行一个系统检测,同时在上线后能够有一个有效的监控程序来保证运行安全。

什么是内存泄漏

在介绍Node.js内存泄漏前,我们应该首先知道什么才是内存泄漏,内存泄漏又包含哪些类型。

内存泄漏概念

内存泄漏也称作“存储渗漏”,用动态存储分配函数,动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。
上面的定义来自百度百科,当然从上面的定义我们就可以了解到内存泄漏简单说,就是占用着系统资源而一直不释放,所导致的系统资源越来越少,从而导致系统异常的一个比较严重的问题,可以使用下面来解释内存泄漏。
Node.js服务程序假定是一次“班级大扫除”,系统内存资源假定为班级的资源“五个扫把”,而利用资源进行工作的“学生”,这里我们假定为进程。当天学校要进行大扫除,每个班级只有五把扫把,每个人都需要完成一部分扫地工作,学生完成后自动给其他人,当所有人完成扫地工作,大扫除结束,老师首次会将扫把分配给五个人,但是这五个人中存在几个同学手握扫把不做事,即使做完事了,也不会将扫把分配给其他人,导致其他人的扫地工作一直无法进行下去,或者由于扫把有限导致打扫工作进行的很漫长。

内存泄漏类型

内存泄漏包含的类型有:常发性、偶发性、一次性、隐式。

  • 常发性
    发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

  • 偶发性
    发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。

  • 一次性
    发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

  • 隐式
    其主要是在调用函数或者模块时,当参数或者输入没有达到界定值时,是不会发生泄漏,当参数或者输入值达到一定时,才会发现内存泄漏,我们称这种为隐式。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天、几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。隐式才是我们本文中所需要去探索,去发现和解决的异常问题。

Node.js内存泄漏会带来的危害

Node.js内存泄漏到底会有哪些危害,既然我们希望去发现和检测内存泄漏,那么我们就必须要首先知道Node.js内存泄漏到底会影响哪些问题。

用户服务异常

一般情况下用户是无法察觉内存泄漏带来的影响,但是对于有些情况下,因为内存泄漏可能导致用户响应很慢,这种情况下对于用户而言无法感受到异常,但是可以普遍感受到服务响应变慢,而且这种情况可能会导致新注册用户丢失等问题。

服务器性能异常

一般情况下,内存泄漏直接的影响就是服务器,服务器会因为内存的不断上涨,从而系统资源可使用的空间越来越小,这样就会慢慢的导致该服务影响到服务器中其他的一些基础服务的运行,从而导致服务器越来越慢,同时导致可使用资源越来越少,直接导致服务器资源耗尽,服务器异常,导致数据丢失等等,比较严重的问题。

常见的 Node.js 内存泄漏问题

这里主要介绍两种关于内存泄漏的代码逻辑,主要是循环引用和无节制循环带来的内存泄漏。

循环引用

这部分在javascript中可能更比较常见,其主要介绍的是说:A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0 ,因此作为回收机制是不会将A和B进行回收的。

var func = function () {}  var el = function () {}  el.func = func;  func.element = el;

例如上面代码中el和func互相引用。而且这种类型的内存泄漏可以说是常发性。

无节制循环

没有对数组有任何限制,并且在数组过大时,没有进行有效的回收处理机制。

模块中的私有方法和属性
任意编写的模块文件中,均会在头和尾部上添加字符串,以形成闭包,然后在require的过程中被调用一次,并且将exports对象存储在内存中,直到进程退出才会回收。这种严格上说并非内存泄漏,只是说需要严格注意模块中的私有变量和方法的使用,避免因为过多的私有变量占用到了过多的系统内存。
假定我们有模块leak.js

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

那么如果我们创建一个test.js来引用该模块,运行时,则会看的leakArray一直变大。

var Mod = require('./leak');  Mod.leak();  Mod.leak();  Mod.leak();  Mod.leak();  Mod.leak();

如果这里我们无节制的调用方法leak时,就可能导致数组过大,从而导致一定的问题。

过大的数组循环
先看下如下代码:

for ( var i = 0; i < 100000000; i++ ) { var user = {};      user.name  = 'outmem';      user.pass  = '123456';      user.email = 'outmem[@outmem](/user/outmem).com';  }

这段代码最主要的原因在于循环太大,直接内存分配到超过v8内存限制数量。由于JavaScript事件循环的执行机制,这段代码没有机会进入下一个事件循环。用setInterval和setTimeout可以进入下一个循环。但是不推荐用setInterval和setTimeout。对于大循环代码,建议最好是分割,然后进行处理,分段进行处理。因为每次都没有效利用好一次循环。一次事件循环,不要超过10ms。

Node.js内存泄漏工具使用实践

这里主要介绍一些常见的Node.js内存泄漏检测工具,并且针对其中的memwatch以及heapdump来进行详细的实践学习。

Node.js内存泄漏工具

node-inspector提供了绑定在Node中的V8分析器和一个基于WebKit Web Inspector的debug界面,大家可以看下这篇博文,其中就是介绍如何应用该工具来检测内存泄漏
http://www.cnblogs.com/ldlchina/p/4762036.html

node-mtrace,它使用了GCC的mtrace工具来分析堆的使用。
https://github.com/Jimbly/node-mtrace
但是该工具提供的是点对点的,如果存在异步函数的话,会比较麻烦,这种可以结合其他工具一起使用会比较方便,大家可以看下github的示例代码

node-heap-dump对V8的堆抓取了一张快照并把所有的东西序列化进一个巨大的JSON文件。它还包含了一些分析研究快照结果的JavaScript工具。这里在memwatch中我们是会应用该工具相应的功能来定位泄漏代码逻辑。

memwatch
是一个专门用来检测和监控内存泄漏的工具,其不仅仅可以在上线前进行扫描监控,也可以在上线后进行有效的内存泄漏检测。
接下来的话,我们就实践应用memwatch来检测内存泄漏的以及通过heapdump抓取GC,进行GC内存分析实践。

memwatch的实践

在学习memwatch之前,首先需要安装配置相应的模块,具体操作可以使用npm install memwatch,下载该模块的时候需要进行编译,因此需要python2.6以上以及需要VS2012以上。如果以上配置遇到问题的时候,可以在我的资料中寻找一篇关于如何解决这两个问题的博文链接http://blog.csdn.net/dan_blog/article/details/50707278
那么接下来我们就来实践应用该模块,首先我们看一下简单的实例代码:

var http = require('http');  var server = http.createServer(function (req, res) {      for (var i=0; i<1000; i++) {          server.on('request', function leakyfunc() {});      }      res.end('Hello World\n');  }).listen(1337, '127.0.0.1');

从代码本身看其是存在一定的问题,随着用户每一次请求,其内存占用都会提供,我们停止请求一段时间时,其内存也不会降低,说明该段代码存在一定的问题。
大家可以前往极客学院视频地址中下载该代码的源码,其中还包括了其他资料
可以运行下该段代码,如果使用http://127.0.0.1:1337不断的进行访问,如果在windows下可以在进程中查看该进程内存的使用情况
这里写图片描述
如果你是在Linux的话,可以首先通过命令查看该进程ID,然后再使用top -p 进程ID

ps -ef | grep node  top -p 12202

随时的查看进程所占用的内存,通过访问你会看到其内存的变化情况,同时发现其内存并不会慢慢释放回来。
既然出现了上面的内存泄漏,那么我们就使用memwatch以及heapdump来做检测和分析,改进后的代码如下。

var http = require('http');  var memwatch = require('memwatch');  var hd       = new memwatch.HeapDiff();  var heapdump = require('heapdump');  var server = http.createServer(function (req, res) {      for (var i=0; i<1000; i++) {          server.on('request', function leakyfunc() {});      }      res.end('Hello World\n');  }).listen(1337, '127.0.0.1');  memwatch.on('leak', function(info) {      var diff = hd.end();      console.log(JSON.stringify(diff));        var file = '/tmp/myapp-' + process.pid + '-' + Date.now() + '.heapsnapshot';      heapdump.writeSnapshot(file, function(err){          if (err) console.error(err);          else console.error('Wrote snapshot: ' + file);      });  });  server.setMaxListeners(0);  console.log('Server running at http://127.0.0.1:1337/. Process PID: ', process.pid);

上面的逻辑中包含了使用memwatch来检测内存泄漏,同时还包含了使用heapdump来抓取内存的实时情况,通过运行如上代码,然后使用压测工具对http://127.0.0.1:1337进行压测,当压测到一定的情况后,在运行窗口你可以看的其内存泄漏的提醒,并在这时候会在file这个目录文件中(如果在windows中最好就修改下file这个文件路径,这里的示例代码是相对Linux环境的)记下当前内存泄漏时的内存GC情况。

GC内存分析

既然获取到该文件后,我们再使用google chrome的工具来分析GC的内存情况,并查看出导致内存泄漏的原因。
这里写图片描述
打开工具后,再使用Load,将我们刚生成的文件加载进去。
加载进去以后关于分析方法,大家可以参考该文章
http://itindex.net/detail/52929-chrome-%E5%BC%80%E5%8F%91-%E5%B7%A5%E5%85%B7
里面有详情的介绍,当然如果想了解更具体的分析方法,大家可以去极客学院上查看本课程的视频学习资料。

总结

这就是本文所介绍的知识,在看完本文以后,大家至少了解什么是内存泄漏,Node.js的内存泄漏会导致哪些问题,以及如何应用memwatch和heapdump来检测和分析内存泄漏问题,同时需要简单了解Chrome中的内存分析工具的使用。