一、常见JS内存泄漏
1.1 全局变量引起的内存泄漏
全局变量使用完毕没有置为null导致内存就无法回收。平常应注意不要引入意外的全局变量,比如定义变量记得加 var声明。
全局变量引发泄露的实例:
<button onclick="createNode()">添加节点</button>
<button onclick="removeNode()">删除节点</button>
<div id="wrapper"></div>
<script>
var text = [];
function createNode() {
text.push(new Array(1000000).join('x'));
var textNode = document.createTextNode("新节点"),
div = document.createElement('div');
div.appendChild(textNode);
document.getElementById("wrapper").appendChild(div);
}
function removeNode() {
var wrapper = document.getElementById("wrapper"),
len = wrapper.childNodes.length;
if (len > 0) {
wrapper.removeChild(wrapper.childNodes[len - 1]);
}
}
</script>
点击添加节点,再点击删除节点,作为一轮操作。通过chrome JS堆动态分配时间轴可以看到每轮操作后,都有JS堆内存得不到释放。
滑动鼠标滚轮,查看其中一段,可以发现字符串"xxx.."没有被回收。选中字符串,在下方Retainers可以看到变量是数组window.text的其中一个元素。结合代码,发现是函数createNode()每次都会把字符串"xxx.."添加到全局变量window.text中。
如果全局变量引用的是DOM,将会导致DOM节点无法回收。Class Filter搜索Detached可以看到许多分离的节点。比如下面这段代码:
<button onclick="updateNodes()">刷新节点</button>
<div id="wrapper"></div>
<script>
var elements =[];
function updateNodes() {
var wrapper = document.getElementById("wrapper"),
len = wrapper.childNodes.length;
if (len) {
for (var j = len - 1; j >= 0; j--) {
wrapper.removeChild(wrapper.childNodes[j]);
}
}
var div,
i = 100,
frag = document.createElement("div");
for (; i > 0; i--) {
div = document.createElement("div");
div.appendChild(document.createTextNode(i + " - " + new Date().toTimeString()));
frag.appendChild(div);
}
wrapper.appendChild(frag);
elements.push(div);
}
</script>
上面这段代码第一次点击刷新节点不会有问题,但第二次点击刷新节点就会有内存泄漏。因为有JS变量指向DOM节点,导致DOM节点无法回收。虽然JS变量只保存了1个节点,但却影响了101个节点的回收,这101个节点因为是这个节点的相关节点而没有被回收(相关节点包括父级节点、父级节点的子节点等)。如下图所示,有101个未被回收的分离节点,这些节点标红显示,说明JS变量引用节点间接影响了这些节点的回收。
点击选中Detached HTMLDivElement,可以查看这些节点被哪个JS变量引用。从下图中可以看出分离节点是被window.elements数组间接引用:
1.2 闭包引起的泄漏
用Meteor 的官方博文 《An interesting kind of JavaScript memory leak》经典的内存泄漏代码作为例子(访问不了可以点击参考文章中的链接)。
<button onclick="replaceThing()">第二次点我就有泄漏</button>
<script>
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) {
console.log("hi");
};
}
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function someMethod() {
console.log('someMessage');
}
};
};
</script>
代码运行之后,点击四次按钮录制的JS堆内存分配时间线如下图所示。选中第一次点击未被回收的JS堆。可以看到字符串"xxx"未被回收,占内存约10%。这个字符串是被originanlThing.longStr引用,originanlThing是在someMethod()函数中定义的,someMethod()又是另一个originalThing的方法...。这就是闭包不断嵌套,可以看到最上面的longStr的Distance是11,距离根节点已经比较远了。如果再多点击几次,查看第一个没有被回收的JS堆,会发现Distance值越来越大。
这段代码为什么会产生泄漏?先来了解下闭包的原理:
同一个函数内部的闭包作用域只有一个,所有闭包共享。在执行函数的时候,如果遇到闭包,会创建闭包作用域内存空间,将该闭包所用到的局部变量添加进去,然后再遇到闭包,会在之前创建好的作用域空间添加此闭包会用到而前闭包没用到的变量。函数结束,清除没有被闭包作用域引用的变量。
上面那段代码泄漏的原因在于有两个闭包:unused和someMethod。unused 这个闭包引用了父作用域中的 originalThing 变量,如果没有后面的 someMethod,则会在函数结束后清除,闭包作用域也跟着清除了。因为后面的 theThing 是全局变量someMethod是全局变量的属性,它引用的闭包作用域(包含了 unused 所引用的 originalThing )不会释放。而随着 replaceThing 不断调用,originalThing 指向前一次的 theThing,而新的theThing.someMethod又会引用originalThing ,从而形成一个闭包引用链,而 longStr是一个大字符串,得不到释放,从而造成内存泄漏。
这个的解决方法就是在函数结束之后将不需要使用的变量置为null。即在replaceThing函数最后加上originalThing = null。
1.3 DOM删除时没有解绑事件
删除DOM时如果没有移除DOM,可能会引起泄漏。下面这段代码在chrome下内存能保持稳定,在IE8下测试内存会不断增长:
<button onclick="createSomeNodes()">增加节点</button>
<button onclick="removeSomeNodes()">删除节点</button>
<div id="wrapper"></div>
<script type="text/javascript">
function createSomeNodes() {
var wrapper = document.getElementById("wrapper"),
outerDiv = document.createElement("div"),
text = document.createTextNode("新节点");
outerDiv.appendChild(text);
for (var i = 10000 ; i > 0; i--) {
var div = document.createElement("div");
div.onclick = function () {
console.log("click")
};
outerDiv.appendChild(div);
}
wrapper.appendChild(outerDiv);
}
function removeSomeNodes () {
var wrapper = document.getElementById("wrapper"),
len = wrapper.childNodes.length;
if (len) {
wrapper.removeChild(wrapper.childNodes[len - 1]);
}
}
</script>
运行上面代码,先点击15次“增加节点“,再点击15次“删除节点”,放置几分钟观察内存。内存最初18M,操作以后上涨到204.806M。(IE8没有找到分析内存消耗的工具,可以借助Window系统任务管理器查看。按Ctrl + Shift + Esc打开任务管理器,找到iexplore.exe进程,查看对应内存。即使只打开一个标签页,也有两个IE进程,一个应该是IE浏览器本身的进程,另一个是标签页对应的进程。如下图所示:)
1.4 被遗忘的定时器
定时器如果不需要使用,记得及时清除。否则会导致定期器回调函数以及内部依赖的变量没有办法及时回收。
<button onclick="changePage()">切换页面</button>
<div id="page1" class=&