本文将探索常见的客户端 JavaScript 内存泄露,以及如何使用 Chrome 开发工具发现问题。 简介 内存泄露是每个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其他应用问题。 1 什么是内存泄露? 本质上,内存泄露可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。 2 JavaScript 内存管理 JavaScript 是一种垃圾回收语言。垃圾回收语言通过周期性地检查先前分配的内存是否可达,帮助开发者管理内存。换言之,垃圾回收语言减轻了“内存仍可用”及“内存仍可达”的问题。两者的区别是微妙而重要的:仅有开发者了解哪些内存在将来仍会使用,而不可达内存通过算法确定和标记,适时被操作系统回收。 3 JavaScript 内存泄露 垃圾回收语言的内存泄露主因是不需要的引用。理解它之前,还需了解垃圾回收语言如何辨别内存的可达与不可达。 4 Mark-and-sweep 大部分垃圾回收语言用的算法称之为 Mark-and-sweep 。算法由以下几步组成:
现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。 不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍被留在激活的 root 树中。在 JavaScript 中,不需要的引用是保留在代码中的变量,它不再需要,却指向一块本该被释放的内存。有些人认为这是开发者的错误。 为了理解 JavaScript 中最常见的内存泄露,我们需要了解哪种方式的引用容易被遗忘。 三种类型的常见 JavaScript 内存泄露 1 意外的全局变量 JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是window。 function foo(arg) {
bar = "this is a hidden global variable";
}
真相是: function foo(arg) { window.bar = "this is an explicit global variable";
}
函数foo内部忘记使用var,意外创建了一个全局变量。此例泄露了一个简单的字符串,无伤大雅,但是有更糟的情况。 另一种意外的全局变量可能由this创建: function foo() { this.variable = "potential accidental global";
}// Foo 调用自己,this 指向了全局对象(window)// 而不是 undefinedfoo();
全局变量注意事项 尽管我们讨论了一些意外的全局变量,但是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或重新分配)。尤其当全局变量用于临时存储和处理大量信息时,需要多加小心。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。 2 被遗忘的计时器或回调函数 在 JavaScript 中使用setInterval非常平常。一段常见的代码: var someResource = getData();
setInterval(function() { var node = document.getElementById('Node'); if(node) { // 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
此例说明了什么:与节点或数据关联的计时器不再需要,node对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource如果存储了大量的数据,也是无法被回收的。 对于观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。 观察者代码示例: var element = document.getElementById('button');function onClick(event) {
element.innerHTML = 'text';
}
element.addEventListener('click', onClick);
对象观察者和循环引用注意事项 老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄露。如今,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用removeEventListener了。 3 脱离 DOM 的引用 有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。 var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};function doStuff() {
image.src = 'http://some.url/image';
button.click(); console.log(text.innerHTML); // 更多逻辑}function removeButton() { // 按钮是 body 的后代元素
document.body.removeChild(document.getElementById('button')); // 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被 GC 回收。}此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|