http://ju.outofmemory.cn/entry/25714
天堂的使者GC说:“不管你的过去和多少人发生了纯洁或是邪恶的关系,只要你愿意斩断以往的一切尘缘,我就能带你到天堂”
最近的工作重心围绕在优化JavaScript代码,减少内存泄露上面。经过这段时间的摸索,对如何控制内存泄露上面有了一些心得,想分享一下。
我们编写大型的JavaScript程序时,没经验的程序员写出的程序肯定会出现大量的内存泄露,内存不能被回收,很容易导致程序崩溃。在PC上可能还表现的不太明显,但是在ipad等移动设备上面运行,也许你会发现,在程序运行过一段时间后,ipad直接跳出浏览器,终止了你的程序。这种情况就说明你程序有严重的内存泄露,IOS认为你的程序存在风险,终止了你的程序。
所以在做Web程序时,你必须关注你的内存使用情况,避免持续的增长。从屁屁曾经悲剧的经验来看,我们必须了解如果避免内存泄露,平时写程序的时候就应该有这个概念,不要等到项目快写完了,再来一次集中的性能优化,等到最后来做,那绝对会非常相当的痛苦!
那么下面,开始吧:
1.Google Chrome Profiles工具跟踪内存使用情况:
“工欲善其事,必先利其器”,要跟踪JavaScript程序内存的使用情况,最先进的工具当属 Webkit内核浏览器的Profiles工具了。你可以使用Safari或Google Chrome,差别不大。屁屁这里使用的是Google Chrome 16 开发者版,profiles工具用(Shift+Ctrl+J)调出。
2.关于JavaScript内存泄露,GC会自动回收没有被引用的对象
内存泄露,在JavaScript程序中的表现就是程序的内存得不到释放,持续的增长。释放内存的工作由GC来做,他的工作宗旨就是:“将不再使用的对象KILL掉,回收占有的内存”。这句话很简单,专业一点讲,就是将没有被任何变量引用的对象回收。GC相当负责的工作着,无奈的是我们这些小虾米不配合GC,随意的把对象赋给一些变量(不光光是外部变量,局部的变量也会犯罪,也会释放不掉!)。所以不是GC不聪明,是我们太傻太天真!因此,为了配合GC的工作,我们要坚决的将那些钉子户藕断丝连的爱情咔嚓掉,把对象将交给GC吧!哎,费换了一大堆,其实就是一句:
去除对象的所有引用就能让GC回收对象
3.去除对象引用深入探究
3.1去除对象引用一般情况,认识Google chrome的Profiles工具
简单的一句话,但是由于JavaScript的事件绑定,闭包等特性,实现起来可不简单啊~
先来上一道小菜,也初识下Profiles工具:
function Library(name){
this.name = name;
}
var PIPI = {
Mapping : [],
get : function(){
return PIPI.Mapping[0];
}
}
var externLib = new Library("0000");
PIPI.Mapping.push(externLib);
(function(){
var lib = PIPI.get();
lib = null;
var lib2 = iT.Mapping[0];
lib2 = null;
})();
这里的 new Library(“0000″);对象有没有回收呢?看看结果吧:
嘿嘿,我们明显看到了两个library对象:一个是函数声明,另外一个才是 new Library(“0000″)对象,从中间那一栏Retaining path,我们可以清楚的看到有哪些路径引用了new Library(“0000″)对象,不出所料,这个对象被两个地方引用了:外部变量externLib以及PIPI.Mapping[0],是不是感觉Profiles工具很犀利? (哎,firefox 的firebug已经失宠咯)
这是屁屁最初写的代码,以为拿到了这个对象,然后赋值为null就可以清除这个对象的引用,然后就回收了…但结果让我苦恼,没用嘛, new Library(“0000″);还是被两个引用持有了,这段代码等于白干!!
现在我才知道,var lib=PIPI.get(); 是新建立了一个局部引用,对象的引用数变成了3,函数执行结束,局部变量销毁了,依然为2。同理,lib2也是一样的道理。
我们很明确的知道了 new Library(“0000″);被两个地方引用了,所以我们要把两个引用都去掉,用下面这段代码解决吧。
iT.Mapping[0] = null;
externLib = null;
最后看一看完整的代码,从中看引用数的变化
function Library(name){
this.name = name;
}
var PIPI = {
Mapping : [],
get : function(){
return PIPI.Mapping[0];
}
}
var externLib = new Library("0000");
PIPI.Mapping.push(externLib);
//此时new Library("0000")对象有2个引用
(function(){
var lib = PIPI.get();
//引用数+1:3
lib = null;
//引用数-1:2
var lib2 = PIPI.Mapping[0];
//引用数+1:3
lib2 = null;
//引用数-1:2
PIPI.Mapping[0] = null;
//引用数-1:1
externLib = null;
//引用数-1:0
})();
当然啦,lib和lib2都是局部变量,不用我们费劲的去手工置为null,函数执行完毕,lib,lib2两个变量自然就销毁了,2个引用也解除了。
哈哈,是不是有点感觉了? 这只是最简单的情况,再深入下去吧
3.2局部变量引用,事件绑定,循环时钟,闭包,
鸟蛋自信的说:“对于局部变量引用的对象,我们可以很放心,函数执行完毕,局部变量被销毁,它引用的对象自然没有别的变量引用了,GC就把他送天堂咯!” 这句话很多人都想当然的认为是正确的,其实不全对!因为他们只考虑了最简单的情况,如果涉及到异步回调,事件绑定等,就错了。在看具体的例子之前,屁屁先指出一些小鸟认识上的误区,就是认为:“我们只要考虑将全局变量的引用去除,局部变量不用管!” 小鸟走弯了方向,我们的关注点不应该在是否全局变量引用了对象,而应该从函数执行,变量的的生命周期来看待这个问题!
第二盘菜是大菜,送上来:
function Library(name){
this.name = name;
}
(function(){
var library = new Library("1111");
document.body.onclick = function(){
console.log("body's click event hold a library instance: " + library.name);
};
var clickHandler = function(){
console.log("body's click event hold a library instance: " + library.name);
}
document.body.addEventListener("click", clickHandler, false);
var interval = setInterval(function(){
console.log("interval hold a library instance: " + library.name);
},1000);
var messageHandler = function(callback){
console.log("messageHandler hold a library instance: " + library.name);
callback && typeof callback ==="function" && callback();
};
window.addEventListener("message", messageHandler, false);
setTimeout(function(){
console.log("clear all reference of library 11111 ");
//这之后的任意一条代码都不能少,少任意一条,new Library("1111")就会被hold住,不能被回收
document.body.onclick = null;
document.body.removeEventListener("click", clickHandler);
clearInterval(interval);
window.removeEventListener("message", messageHandler);
},3000);
})();
我们在自动执行的匿名函数中通过var library = new Library(“1111″)声明了对象new Library(“1111″),然后做了一系列的事件绑定,而setTimeout函数中展现了如何清除所有的引用,少任何一条都会导致new Library(“1111″)被hold住。怎么样?大家是不是经常很开心的用着闭包,然后忘记了去除那些事件绑定或者循环时钟呢。让我们来看看3秒之前,也就是执行setTimeout之前时,程序的内存快照:
从retaining path,我们可以窥探出大概有那些路径引用了new Library(“1111″)对象,就是那些事件处理函数,interval时钟。这个例子是个典型的对象被局部变量引用,但是需要我们手动清除的例子。所以说“我们只要把注意点放在清除全局变量的引用上面”这个观点是错误的。对象的引用只和对象的生命周期有关,在JavaScript中,就和函数的执行有关。在没有闭包的情况下,函数执行完毕后,函数会销毁他的执行环境,变量自然就被销毁了,引用就不存在了。而这个例子中,所有的事件处理函数,循环时钟,无不是产生了闭包,闭包中的变量持有了对new Library(“1111″)的引用。只要这些闭包不销毁,就会一直引用new Library(“1111″),GC当然就不会回收了。
3.3邪恶闭包
再看看下面,屁屁继续上菜咯,看看闭包是多么的邪恶!!
function Library(name){
this.name = name;
}
function BadBoy(name){
this.name = name;
}
function Book(name){
this.name = name;
}
(function(){
var library = new Library("2222");
var badBoy = new BadBoy("PIPI");
var book = new Book("Professional JavaScript for Web Developers");
document.body.onclick = function(){
console.log("body's click event hold a library instance: " + library.name);
};
var clickHandler = function(){
console.log("body's click event hold a library instance: " + library.name);
};
document.body.addEventListener("click", clickHandler, false);
var interval = setInterval(function(){
console.log("interval hold a library instance: " + library.name);
},1000);
var messageHandler = function(callback){
console.log("message hold a library instance: " + library.name);
callback && typeof callback ==="function" && callback();
};
window.addEventListener("message", messageHandler, false);
setTimeout(function(){
console.log(badBoy.name);
console.log("clear all reference of library 2222 ");
document.body.onclick = null;
document.body.removeEventListener("click", clickHandler);
clearInterval(interval);
window.removeEventListener("message", messageHandler);
},3000);
var interval2 = setInterval(function(){
var libraryTemp = new Library("3333");
console.log(libraryTemp.name);
console.log("interval2 hold a library instance: ");
},1000);
})();
let’s look snapshot:
再重复说一次,chrome16的Profiles中的结果,一定有一个是声明(老版的chrome不会显示,例如chomre14正式版),所以这个结果中,Book其实没有对象示例,Library有一个,BadBoy有一个。结果是不是让人很抓狂!!!对于new Library(“2222″)这个对象,我们明明按照上面的方法去除了所有引用啊。最后一个interval2又没有通过闭包引用new Library(“2222″)这个对象,而新加入的局部变量 new BadBoy(“PIPI”); 被setTimeout中的回调引用了,但是setTimeout执行完后应该释放啊!!new Library(“3333″)倒是正常的释放了。new Book(“Professional JavaScript for Web Developers”);也是正常释放了。
关键在下面这段
var interval2 = setInterval(function(){
var libraryTemp = new Library("3333");
console.log(libraryTemp.name);
console.log("interval2 hold a library instance: ");
},1000);
如果我们不加入这一段,或者执行了clearInterval(interval2)。那么Library,BadBoy,Book的所有引用都能去除。神奇吧!其中的原因,应该是interval2的处理函数是闭包的缘故,导致整个匿名函数一直得不到释放,从而使那些被其它闭包引用的对象new Library(”2222“), new BadBoy(“PIPI”)也得不到释放。如果执行了 cleartInterval(interval2),匿名函数中的所有闭包销毁了,匿名函数执行环境才得到了释放,所有的引用就清除了。具体的细节我不分析了,请查阅函数的作用域链,函数的执行环境,函数的活动对象等资料吧,最好看《JavaScript高级程序设计第二版》,有详细的介绍。
对于闭包的这个特性,我们不禁要捏把汗啊….以后写程序可要多个心眼啊。