Javascript内存泄露
当一个系统无法正确管理它的内存分配时,我们称之为内存泄露。内存泄露是个bug,它会导致系统性能下降甚至挂掉。 IE存在很多漏洞,其中最严重当属js脚本执行。当一个DOM对象引用了一个js对象(事件句柄函数),同时这个js对象又有对该DOM对象的引用,此时就形成了一个循环引用。这本身是没有问题的。如果没有别的对该DOM和js对象的引用,GC将会同时回收它们,释放内存。GC可以识别出循环引用。不幸的是,IE的DOM没有被JS脚本管理。它有自己的一个内存管理,无法识别出循环引用。结果当循环引用存在时,无法进行内存回收。最终这会导致内存耗尽,浏览器崩溃。 下面这段代码将会创建10000个DOM元素,通过队列控制始终保留最近添加的10个元素,旧的被删除掉。打开Windows管理器,该网页所用内存几乎保持稳定。
(function (limit, delay) {
var queue = new Array(10);
var n = 0;
function makeSpan(n) {
var s = document.createElement('span');
document.body.appendChild(s);
var t = document.createTextNode(' ' + n);
s.appendChild(t);
return s;
}
function process(n) {
queue.push(makeSpan(n));
var s = queue.shift();
if (s) {
s.parentNode.removeChild(s);
}
}
function loop() {
if (n < limit) {
process(n);
n += 1;
setTimeout(loop, delay);
}
}
loop();
})(10000, 10);
接下来,运行第二段代码,同样的功能,此时给每一个元素增加一个click事件。在Mozilla和Opera中,内存使用稳定,但在IE中,内存每秒钟都在增长,引发了内存泄露。这个漏洞一般不易察觉。但在Ajax中变得很常见,随着在该页面呆的时间越长,改变的地方越多,那么越容易失败导致崩溃。
(function (limit, delay) {
var queue = new Array(10);
var n = 0;
function makeSpan(n) {
var s = document.createElement('span');
document.body.appendChild(s);
var t = document.createTextNode(' ' + n);
s.appendChild(t);
s.onclick = function (e) {
s.style.backgroundColor = 'red';
alert(n);
};
return s;
}
function process(n) {
queue.push(makeSpan(n));
var s = queue.shift();
if (s) {
s.parentNode.removeChild(s);
}
}
function loop() {
if (n < limit) {
process(n);
n += 1;
setTimeout(loop, delay);
}
}
loop();
})(10000, 10);
由于IE无法解决循环引用问题,如果我们明确地打破这种循环引用,那么IE将会回收内存。IE的解释是:闭包是引起内存泄露的原因。这种方式当然是错的,但它导致了IE给了开发者错误的处理bug方式。在DOM中解除循环引用很简单,但实际上在JS脚本中不可能解除。 当我们处理完一个元素节点时,我们必须将它的事件句柄置为null,以解决这一问题。 因此我们可以写一个通用的purge函数。 purge函数将DOM节点作为参数。遍历该节点的属性,如果发现是函数,那么置为null,这将解除循环引用,使得内存得以释放。它同时会遍历该节点的后代元素执行此操作。该函数只针对IE起作用,在移除任何节点、removeChild函数或是设置innerHTML属性前执行此函数。
function purge(d) {
var a = d.attributes, i, l, n;
if (a) {
for (i = a.length - 1; i >= 0; i -= 1) {
n = a[i].name;
if (typeof d[n] === 'function') {
d[n] = null;
}
}
}
a = d.childNodes;
if (a) {
l = a.length;
for (i = 0; i < l; i += 1) {
purge(d.childNodes[i]);
}
}
}
最后来看第三段代码,在删除DOM元素之前调用了purge函数。
(function (limit, delay) {
var queue = new Array(10);
var n = 0;
function makeSpan(n) {
var s = document.createElement('span');
document.body.appendChild(s);
var t = document.createTextNode(' ' + n);
s.appendChild(t);
s.onclick = function (e) {
s.style.backgroundColor = 'red';
alert(n);
};
return s;
}
function process(n) {
queue.push(makeSpan(n));
var s = queue.shift();
if (s) {
purge(s);
s.parentNode.removeChild(s);
}
}
function loop() {
if (n < limit) {
process(n);
n += 1;
setTimeout(loop, delay);
}
}
loop();
})(10000, 10);
IE7及以下中存在此问题
GC原理
垃圾回收机制原理其实很简单:找出那些不再使用的变量,然后释放其内存。为此,垃圾收集器会按照一定的触发条件周期性的触发这个过程。垃圾回收最重要的一点就是确定如何确定垃圾对象,实现中主要有两种机制
- 标记清除 – 由根对象开始标记可到达对象,然后对不可达对象进行回收
- 引用计数 —维护对象的引用计数,实现上很难避免循环引用带来的困扰
标记清除
- 当变量进入环境(例如,声明变量)时,这个变量标记为“进入环境”。当变量离开环境时,这将其标记为“离开环境”
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记
- 去掉环境中变量以及被环境中的变量引用的变量标记。而在此之后仍带有标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了
- 垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间
IE,Firefox,Opera,Chrome和Safari的目前都是使用的标记清除回收策略
引用计数
- 跟踪记录每个值被引用的次数。当声明一个变量并将引用类型的值赋给该变量时,则这个值的引用次数就是1
- 如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得另外一个值,则这个值的引用次数减1
- 当这个值的引用次数变成0时,则说明没有办法访 问这个值了,因此就可以将其占用的内存空间回收回来。这样当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存
该回收机制会有严重的问题:循环引用 IE中有一部分对象并不是原生的Javascript对象。例如:其BOM和DOM对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。因此,即使,IE的Javascript引擎是使用标记清除策略来实现的,但Javascript访问COM对象依然基于引用计数策略的,这就是上面三段代码所演示的效果,解决方案就是人为地将其置为null,使其计数减一。IE9把BOM和DOM对象转换成了真正的Javascript对象
IE的垃圾回收
IE6垃圾回收 - 根据内存分配量运行的,具体一点说就是256个变量、4096个对象(或数组)和数组元素(slot)或者64KB的字符串。达到上述任何一个临界值,垃圾收集器就会运行。问题在于如果一个脚本中包含那么多 变量,那么该脚本很可能会在其生命中起一直保持那么多的变量,垃圾收集器就可能不得不频繁的运行。
IE7垃圾回收 - IE7中的各项临界值在初始化时与IE6相等。如果例程回收的内存分配量低于15%,则变量 、字面量和(或)数组元素的临界值就会加倍。如果例程回收了85%的内存分配量,则将各种临界重置会默认值。这一看似简单的调整,极大地提升了IE在运行 包含大量JavaScript的页面时的性能。
强制垃圾回收 - 有的浏览器中可以触发垃圾收集过程,在IE中,调用window.CollectGarbage()方法会立即指向垃圾收集。
WebKit的垃圾回收
主要步骤为
- 标记对象
- 清除
- 压缩空间
相关代码在heap.cpp中
void Heap::collectAndSweep(HeapOperation collectionType)
{
if (!m_isSafeToCollect)
return;
collect(collectionType);
SamplingRegion samplingRegion("Garbage Collection: Sweeping");
DeferGCForAWhile deferGC(*this);
m_objectSpace.sweep();
m_objectSpace.shrink();
sweepAllLogicallyEmptyWeakBlocks();
}