前言
js中的自动垃圾回收机制让我们可以不用关心内存管理,但在一些情况下会导致内存泄漏,js中的垃圾收集机制主要有两种方式。
1.引用计数垃圾收集
在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。
例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。 如果没有其他对象指向它了,说明该对象已经不再需了。
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”的原始引用o被o2替换了
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 最初的对象现在已经是零引用了
// 他可以被垃圾回收了
// 然而它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
引用计数方法存在一个致命的问题:循环引用。如果两个对象相互引用,尽管它们已不再使用,垃圾回收不会进行回收,导致内存泄漏。比如下面这个例子
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o 这里 出现循环引用
return "azerty";
}
f();
再来看一个实际的例子:
var div = document.createElement("div");
div.onclick = function() {
console.log("click");
};
上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。 此时变量 div 有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。 一个循序引用出现了,按上面所讲的算法,该部分内存无可避免的泄露了。
2.标记清除垃圾收集
标记清除,简单来说,就是从根部(在js中就是全局对象)出发,定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用后。哪些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
可以解决循环引用问题。
工作流程:
- 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
- 从根部出发将能触及到的对象的标记清除。
- 那些还存在标记的变量被视为准备删除的变量。
- 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。
内存泄漏识别方法
chrome
- 打开开发者工具,选择 Performance 面板
- 在顶部勾选 Memory
- 点击左上角的 record 按钮
- 在页面上进行各种操作,模拟用户的使用情况
- 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况
node
console.log(process.memoryUsage());
// {
// rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772
// }
process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。
该对象包含四个字段,单位是字节,含义如下:
- rss(resident set size):所有内存占用,包括指令区和堆栈。
- heapTotal:"堆"占用的内存,包括用到的和没用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎内部的 C++ 对象占用的内存。
判断内存泄漏,以heapUsed字段为准
常见内存泄漏案例
局部变量泄漏成为全局变量
function foo() {
bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
this.bar2 = 'some text' // 全局变量 => window.bar2
}
foo();
定时器未销毁
var serverData = loadData()
setInterval(function(){
var render = document.getElementById('renderer');
if(renderer){
renderer.innerHTML = JSON.stringify(serverData);
}
}, 500)
闭包
在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。 下面这种情况下,闭包也会造成内存泄露:
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 的闭包。
这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。
当这段代码被反复执行时,内存会持续增长。
dom引用
var elements = {
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
document.body.removeChild(document.getElementById('image'));
// 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}
```
上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。
另外需要注意的一个点是,对于一个 Dom 树的叶子节点的引用。
举个例子: 如果我们引用了一个表格中的td元素,一旦在 Dom 中删除了整个表格,我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素。
但是事实上,这个 td 元素是整个表格的一个子元素,并保留对于其父元素的引用。
这就会导致对于整个表格,都无法进行内存回收。所以我们要小心处理对于 Dom 元素的引用。