收集所有存在内存泄漏的所有case。哪怕一个字节也不放过。
Case1:无限制增长的数组
var leakArray = [];
exports.leak = function () {
leakArray.push("leak" + Math.random());
};
这个大概是最常见的内存泄漏案例。只要变量leakArray
不被回收,内存就有可能无限上涨,且不被回收。
解决方案
确认你push元素的数组对象是可以随着作用域执行结束回收。
Case2:无限制设置属性和值
这个情形在缓存对象中经常出现:
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
memoize方法主要是用来解决昂贵CPU耗用的js运算的。它十分有用。但是在后端使用的时候要十分小心。因为memo对象是不会回收的,每次的参数不同都会在这个对象上添加属性和值。
这个案例中,memo对象被当作了缓存来使用,一直无法得到回收。
解决方案
关于如何规避这种无限制缓存的问题,请移步到我另开的帖子中看解决方案:http://cnodejs.org/topic/4fafc843e7656c60680306f9
Case3:任何模块内的私有变量和方法均是永驻内存的
(function (exports, require, module, __filename, __dirname) {
var circle = require('./circle.js');
console.log('The area of a circle of radius 4 is ' + circle.area(4));
exports.get = function () {
return circle();
};
});
任意编写的模块文件中,均会在头和尾部上添加字符串,以形成闭包,然后在require的过程中被调用一次,并且将exports对象存储在内存中,直到进程退出才会回收。
这个案例中,只是内存不会回收,但一般不会造成内存泄漏。需要注意的是私有变量不要通过exports上的方法为其添加内存占用。
第一个案例其实就是由于这个原因造成的。
Case4: 大循环,无GC机会
第四个案例来自于http://cnodejs.org/topic/4fcd020be5e72c25180032e5。
//OOM测试
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。
在Node下有一个特殊的方法,process.nextTick();
for ( var i = 0; i < 100000000; i++ ) {
process.nextTick(function () {
var user = {};
user.name = 'outmem';
user.pass = '123456';
user.email = 'outmem[@outmem](/user/outmem).com';
});
}
不过这样的效率可能不够好。因为每次都没有效利用好一次循环。
一个建议是,一次事件循环,不要超过10ms。太长时间的事件循环,不仅会存在oom的风险,还会阻塞后续IO的启动。
Event listener 上的内存泄露比较难发现, 每个listener都在libuv中有一个引用, 只要emitter还在, listeners都不会回收, 而且listener中所用到的外部变量也都不会被回收.
不过我举不出什么有价值的例子.
很好的主题,期待下文
能否附带防范思路、替代方案神马的?
我有话要问,第一个例子应该不能叫内存泄露吧,在leakArray这个变量的scope一直存在的前提下,这个变量所引用的内存是不会释放的。这个没错,但是如果这个scope不存在了,比如这个scope在一个函数里面,那么当调用完这个函数以后,leakArray这个变量是自动释放的。当然,如果这个函数里有一个闭包函数引用了这个变量,那么应该在这个闭包函数被调用后,就自动释放了。
所以,我觉得第一个例子完全可以不必列入内存泄露的行列,希望我的理解没有问题。
新手飘过。
PS.祝nodeClub生意兴隆。
我列举例子的意思就是这个变量所在的scope不会释放。
从官方的http模块的一路更新看来,要写一个完整没有内存泄漏的应用,得处处小心。
找个热门模块的例子说明,看看 connect.static 模块的最近一次更新,也是修复内存泄漏问题: https://github.com/senchalabs/connect/commit/3e7dc9b5a44c9633506a34f0d2d25bdd079a3a9f
一个以为简单的stream pipe,要做到非常完善,考虑的地方真不少。
非常多的bug修复,都是因为这个原因。
javascript1.7里的let 关键字是否可以在一定程度上规避闭包导致的内存泄露?
这个指望不上吧。
这个要认真学习,谢谢分享
因为module.exports这个家伙是不会被销毁的,所以尽量通过this去间接引用你的静态私有变量,而不是通过exports去搞,这样当this销毁的时候,那些原本可能会永驻内存的“静态私有变量”就finally释放了. 或者再暴力点直接提供一个清空私有变量的方法, 但这样会让逻辑变的更复杂. 虽然是老贴,但是第一次看到…
好贴
一次循环超过一定时间会造成案例4的mem leak吗? 我刚用heapdump发现我的 类似 while(1) {work()} 的代码是有内存泄露的, 改成 nextTick的就没有了。
3年前的文章到现在排版也没乱,赞!
@hit9 第四个例子要讲的跟时间无关,他想表达的是,gc 会发生在 node 事件循环的间隙,但如果循环一直是 cpu 密集型,则临时使用的变量就没有时间被 gc 了。
如果你的 work()
中会持续产生比较大的对象,那么 while 只要一直继续,gc 就不会触发。有些本来应该被 gc 掉的临时变量,就不会被回收。
@hit9 欢迎贴个 gist 出来讨论
@alsotang 建议给旧的文章加个标记,不看时间真不知道是3年前。
很好的主题。
有没有内存泄露检测的专题
好贴
还真没注意是三年前 的
还是赞一个。。
我晕,node竟然还有内存泄露的问题……
3年前!
垃圾回收问题,不要乱用闭包,自循环内变量占用内存太大,每次都不会释放,容易爆内存,最好节点放外部,cron每天夜里重启下服务很必要