![84cd41e031dfcc934a2400cc1c5edca5.png](https://img-blog.csdnimg.cn/img_convert/84cd41e031dfcc934a2400cc1c5edca5.png)
在程序运行过程中不再用到的内存,没有及时释放,会出现内存泄漏(memory leak),会造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
而内存泄漏是每个开发人员最终必须面对的问题。 即使使用内存管理语言,比如C语言有着malloc() 和 free() 这种低级内存管理语言也有可能出现泄露内存的情况。
这很麻烦,所以为了减轻编程中的负担,大多数语言提供了自动内存管理,这被称为"垃圾回收机制"(garbage collector)。
垃圾回收机制
现在各大浏览器通常采用的垃圾回收有两种方法:标记清除(mark and sweep)、引用计数(reference counting)。
1、标记清除
这是javascript中最常用的垃圾回收方式。
工作原理:当变量进入执行环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
- 去掉环境中的变量以及被环境中的变量引用的变量的标记。
- 之后再被加上标记的变量将被视为准备删除的变量。
- 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
2、引用计数
工作原理:跟踪记录每个值被引用的次数。
工作流程:
- 将一个引用类型的值赋值给这个声明了的变量,这个引用类型值的引用次数就是1。
- 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1。
- 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减1
- 当引用次数变成0时,就表示这个值不再用到了。
- 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。
但如果一个值不再需要了,引用数却不为0
,垃圾回收机制无法释放这块内存,会导致内存泄漏。
var arr = [1, 2, 3];
console.log('hello miqilin');
上面代码中,数组[1, 2, 3]
会占用内存,赋值给了变量arr
,因此引用次数为1
。尽管后面的一段代码没有用到arr
,它还是会持续占用内存。
如果增加一行代码,解除arr对[1, 2, 3]引用,这块内存就可以被垃圾回收机制释放了。
var arr = [1, 2, 3];
console.log('hello miqilin');
arr = null;
上面代码中,arr
重置为null
,就解除了对[1, 2, 3]
的引用,引用次数变成了0
,内存就可以释放出来了。
因此,并不是说有了垃圾回收机制,程序员就无事一身轻了。你还是需要关注内存占用:那些很占空间的值,一旦不再用到,你必须检查是否还存在对它们的引用。如果是的话,就必须手动解除引用。
接下来,我将介绍四种常见的JavaScript 内存泄漏及如何避免。目前水平有限,借鉴了国外大牛的文章了解这几种内存泄漏,原文链接:https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec
四种常见的 JavaScript 内存泄漏
1.意外的全局变量
未定义的变量会在全局对象创建一个新变量,对于在浏览器的情况下,全局对象是window。 看以下代码:
function foo(arg) {
bar = "this is a hidden global variable";
}
函数foo
内部使用var
声明,实际上JS会把bar挂载在全局对象上,意外创建一个全局变量。等同于:
function foo(arg) {
window.bar = "this is an explicit global variable";
}
在上述情况下, 泄漏一个简单的字符串不会造成太大的伤害,但它肯定会更糟。
另一种可以创建偶然全局变量的情况是this:
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();
解决方法:
在 JavaScript 文件头部加上 'use strict'
,使用严格模式避免意外的全局变量,此时上例中的this指向undefined
。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
2.被遗忘的计时器或回调函数
在JavaScript中使用setInterval非常常见。
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
} }, 1000);
上面的代码表明,在节点node或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当node节点被移除后,interval 仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick); // Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。其中IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。
但是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用removeEventListener
了。
诸如jQuery之类的框架和库在处理节点之前会删除侦听器(当使用它们的特定API时)。 这由库内部处理,并确保不会产生任何泄漏,即使在有问题的浏览器(如旧版Internet Explorer)下运行也是如此。
3.闭包
JavaScript 开发的一个关键是闭包:这是一个内部函数,它可以访问外部(封闭)函数的变量。由于 JavaScript 运行时的实现细节,用下边这种方式可能会造成内存泄漏:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: newArray(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
每次调用replaceThing
,theThing
得到一个包含一个大数组和一个新闭包(someMethod
)的新对象。同时,变量unused
是一个引用originalThing
的闭包(先前的replaceThing
又调用了theThing
)。someMethod
可以通过theThing
使用,someMethod
与unused
分享闭包作用域,尽管unused
从未使用,它引用的originalThing
迫使它保留在内存中(防止被回收)。需要记住的是一旦一个闭包作用域被同一个父作用域的闭包所创建,那么这个作用域是共享的。
所有这些都可能导致严重的内存泄漏。当上面的代码片段一次又一次地运行时,你可以看到内存使用量的急剧增加。当垃圾收集器运行时,也不会减少。一个链接列表闭包被创建(在这种情况下 theThing 变量是根源),每一个闭包作用域对打数组进行间接引用。
解决方法:
在 replaceThing
的最后添加 originalThing = null
。将所有联系都切断。
4.脱离 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);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
如果代码中保存了表格某一个<td>
的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的<td>
以外的其它节点。实际情况并非如此:此<td>
是表格的子节点,子元素与父元素是引用关系。由于代码保留了<td>
的引用,导致整个表格仍待在内存中。所以保存 DOM 元素引用的时候,要小心谨慎。
避免内存泄漏
在局部作用域中,等函数执行完毕,变量就没有存在的必要了,js垃圾回收机制很快做出判断并且回收,但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量。
我们在使用闭包的时候,就会造成严重的内存泄漏,因为闭包的原因,局部变量会一直保存在内存中,所以在使用闭包的时候,要多加小心。
Resources
- http://www-bcf.usc.edu/~dkempe/CS104/08-29.pdf
- https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156
- http://www.nodesimplified.com/2017/08/javascript-memory-management-and.html
如果有别的关于内存泄漏好的资源,可以分享给我嘛谢谢了~
本人Github链接如下,欢迎各位Star
https://github.com/miqilin21/miqilin21.github.io