什么是内存泄漏
内存泄漏基本上就是不再被应用需要的内存,由于某种原因,没有被归还给操作系统或者进入可用内存池。
编程语言喜欢不同的管理内存方式。然而,一段确定的内存是否被使用是一个不可判断的问题。换句话说,只有开发者才能弄清楚,是否一段内存可以被还给操作系统。
某些编程语言为开发者提供了释放内存功能。另一些则期待开发者清楚的知道一段内存什么时候是没用的。Wikipedia有一篇非常好的关于内存管理的文章。
4种常见的JavaScript内存泄漏
1:全局变量
JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是 window 。换句话说:
-
function foo(arg) {
-
bar = "some text";
-
}
等同于
-
function foo(arg) {
-
window.bar = "some text";
-
}
如果 bar 被假定只在 foo 函数的作用域里引用变量,但是你忘记了使用 var 去声明它,一个意外的全局变量就被声明了。
在这个例子里,泄漏一个简单的字符串不会造成很大的伤害,但是它确实有可能变得更糟。
另外一个意外创建全局变量的方法是通过 this :
-
function foo() {
-
this.var1 = "potential accidental global";
-
}
-
// Foo作为函数调用,this指向全局变量(window)
-
// 而不是undefined
-
foo();
为了防止这些问题发生,可以在你的JaveScript文件开头使用 'use strict'; 。这个可以使用一种严格的模式解析JavaScript来阻止意外的全局变量。
除了意外创建的全局变量,明确创建的全局变量同样也很多。这些当然属于不能被回收的(除非被指定为null或者重新分配)。特别那些用于暂时存储数据的全局变量,是非常重要的。如果你必须要使用全局变量来存储大量数据,确保在是使用完成之后为其赋值 null或者重新赋其他值。
2: 被遗忘的定时器或者回调
在JavaScript中使用 setInterval 是十分常见的。
大多数库,特别是提供观察器或其他接收回调的实用函数的,都会在自己的实例无法访问前把这些回调也设置为无法访问。但涉及 setInterval 时,下面这样的代码十分常见:
-
var serverData = loadData();
-
setInterval(function() {
-
var renderer = document.getElementById('renderer');
-
if(renderer) {
-
renderer.innerHTML = JSON.stringify(serverData);
-
}
-
}, 5000); //每5秒执行一次
定时器可能会导致对不需要的节点或者数据的引用。
renderer 对象在将来有可能被移除,让interval处理器内部的整个块都变得没有用。但由于interval仍然起作用,处理程序并不能被回收(除非interval停止)。如果interval不能被回收,它的依赖也不可能被回收。这就意味着 serverData ,大概保存了大量的数据,也不可能被回收。
在观察者的情况下,在他们不再被需要(或相关对象需要设置成不能到达)的时候明确的调用移除是非常重要的。
在过去,这一点尤其重要,因为某些浏览器(旧的IE6)不能很好的管理循环引用(更多信息见下文)。如今,大部分的浏览器都能而且会在对象变得不可到达的时候回收观察处理器,即使监听器没有被明确的移除掉。然而,在对象被处理之前,要显式地删除这些观察者仍然是值得提倡的做法。例如:
-
var element = document.getElementById('launch-button');
-
var counter = 0;
-
function onClick(event) {
-
counter++;
-
element.innerHtml = 'text ' + counter;
-
}
-
element.addEventListener('click', onClick);
-
// 做点事
-
element.removeEventListener('click', onClick);
-
element.parentNode.removeChild(element);
-
// 当元素被销毁
-
//元素和事件都会即使在老的浏览器里也会被回收
如今的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,先调用 removeEventListener 再删节点并非严格必要。
jQuery等框架和插件会在丢弃节点前删除监听器。这都是它们内部处理,以保证不会产生内存泄漏,甚至是在有问题的浏览器(没错,IE6)上也不会。
3: 闭包
闭包是JavaScript开发的一个关键方面:一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行时实现的不同,它可能以下面的方式造成内存泄漏:
-
var theThing = null;
-
var replaceThing = function () {
-
var originalThing = theThing;
-
var unused = function () {
-
if (originalThing) // 引用'originalThing'
-
console.log("hi");
-
};
-
theThing = {
-
longStr: new Array(1000000).join('*'),
-
someMethod: function () {
-
console.log("message");
-
}
-
};
-
};
-
setInterval(replaceThing, 1000);
这段代码做了一件事:每次 ReplaceThing 被调用, theThing 获得一个包含大数组和新的闭包( someMethod )的对象。同时,变量 unused 保持了一个引用 originalThing ( theThing 是上次调用 replaceThing 生成的值)的闭包。已经有点困惑了吧?最重要的事情是 一旦为同一父域中的作用域产生闭包,则该作用域是共享的。
这里,作用域产生了闭包, someMethod 和 unused 共享这个闭包中的内存。 unused 引用了 originalThing 。尽管 unused 不会被使用, someMethod 可以通过 theThing 来使用 replaceThing 作用域外的变量(例如某些全局的)。而且 someMethod 和 unused 有共同的闭包作用域, unused 对 originalThing 的引用强制 oriiginalThing 保持激活状态(两个闭包共享整个作用域)。这阻止了它的回收。
当这段代码重复执行,可以观察到被使用的内存在持续增加。垃圾回收运行的时候也不会变小。从本质上来说,闭包的连接列表已经创建了(以 theThing 变量为根),这些闭包每个作用域都间接引用了大数组,导致大量的内存泄漏。
这个问题被Meteor团队发现,他们有描述了闭包大量的细节。
4: DOM外引用
有的时候在数据结构里存储DOM节点是非常有用的,比如你想要快速更新一个表格几行的内容。此时存储每一行的DOM节点的引用在一个字典或者数组里是有意义的。此时一个DOM节点有两个引用:一个在dom树中,另外一个在字典中。如果在未来的某个时候你想要去移除这些排,你需要确保两个引用都不可到达。
-
var elements = {
-
button: document.getElementById('button'),
-
image: document.getElementById('image')
-
};
-
function doStuff() {
-
image.src = 'http://example.com/image_name.png';
-
}
-
function removeImage() {
-
//image是body元素的子节点
-
document.body.removeChild(document.getElementById('image'));
-
//这个时候我们在全局的elements对象里仍然有一个对#button的引用。
-
//换句话说,buttom元素仍然在内存中而且不能被回收。
-
}
当涉及DOM树内部或子节点时,需要考虑额外的考虑因素。例如,你在JavaScript中保持对某个表的特定单元格的引用。有一天你决定从DOM中移除表格但是保留了对单元格的引用。人们也许会认为除了单元格其他的都会被回收。实际并不是这样的:单元格是表格的一个子节点,子节点保持了对父节点的引用。确切的说,JS代码中对单元格的引用造成了 整个表格被留在内存中了 ,所以在移除有被引用的节点时候要当心。
我们在SessionStack努力遵循这些最佳实践,因为:
一旦你整合essionStack到你的生产应用中,它就开始记录所有的事情:DOM变化、用户交互、JS异常、堆栈跟踪、失败的网络请求、调试信息,等等。
通过SessionStack,你可以回放应用中的问题,看到问题对用户的影响。所有这些都不会对你的应用产生性能的影响。因为用户可以重新加载页面或者在应用中跳转,所有的观察者、拦截器、变量分配都必须合理处置。以免造成内存泄漏,也预防增加整个应用的内存占用。