创建内存泄漏(js的问题)
如果没有有意识地编写代码来避免内存泄漏,那么内存泄漏几乎是不可避免的JavaScript问题。它们的发生方式有很多种,所以我们只重点介绍几种比较常见的情况。
内存泄漏实例1:对不存在的对象的悬空引用
考虑以下代码:
var theThing = null;
var replaceThing = function () {
var priorThing = theThing;
var unused = function () {
// 'unused'是'priorThing'被引用的唯一地方。
// 但'unused'从未被调用过
if (priorThing) {
console.log("hi");
}
};
theThing = {
longStr: new Array(1000000).join('*'), // 创建一个1MB的对象
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000); // 每秒钟调用一次 "replaceThing"。
如果你运行上述代码并监测内存使用情况,你会发现你有一个明显的内存泄漏,每秒泄漏整整一兆字节!而即使是手动垃圾收集器(GC)也无济于事。因此,看起来我们每次调用 replaceThing
都会泄漏 longStr
。但是为什么呢?
每个theThing
对象包含它自己的1MB longStr
对象。每一秒钟,当我们调用 replaceThing
时,它都会在 priorThing
中保持对先前 theThing
对象的引用。
但是我们仍然认为这不会是一个问题,因为每次通过,先前引用的priorThing
将被取消引用(当priorThing
通过priorThing = theThing;
被重置时)。而且,只在 replaceThing
的主体和unused
的函数中被引用,而事实上,从未被使用。
因此,我们又一次想知道为什么这里会有内存泄漏。
为了理解发生了什么,我们需要更好地理解JavaScript的内部工作。实现闭包的典型方式是,每个函数对象都有一个链接到代表其词法作用域的字典式对象。如果在replaceThing
里面定义的两个函数实际上都使用了priorThing,
那么它们都得到了相同的对象就很重要,即使priorThing
被反复赋值,所以两个函数都共享相同的词法环境。但是一旦一个变量被任何闭包使用,它就会在该作用域内所有闭包共享的词法环境中结束。而这个小小的细微差别正是导致这个可怕的内存泄露的原因。
内存泄漏实例2:循环引用
考虑下面代码:
function addClickHandler(element) {
element.click = function onClick(e) {
alert("Clicked the " + element.nodeName)
}
}
这里,onClick
有一个闭包,保持对element
的引用(通过element.nodeName
)。通过将onClick
分配给element.click
,循环引用被创建;即: element
→ onClick
→ element
→ onClick
→ element
...
有趣的是,即使 element
被从DOM中移除,上面的循环自引用也会阻止 element 和onClick
被收集,因此会出现内存泄漏。
避免内存泄漏:要点
JavaScript的内存管理(尤其是垃圾回收)主要是基于对象可达性的概念。
以下对象被认为是可达的,被称为 "根":
-
从当前调用堆栈的任何地方引用的对象(即当前被调用的函数中的所有局部变量和参数,以及闭包作用域内的所有变量)
-
所有全局变量
只要对象可以通过引用或引用链从任何一个根部访问,它们就会被保留在内存中。
浏览器中有一个垃圾收集器,它可以清理被无法到达的对象所占用的内存;换句话说,当且仅当GC认为对象无法到达时,才会将其从内存中删除。不幸的是,很容易出现不再使用的 "僵尸 "对象,但GC仍然认为它们是 "可达的"。