目录

  • 什么是内存泄漏
  • 导致内存泄漏的原因
  • 一、意外地创建全局变量
  • 例子1:没有使用var
  • 例子2:this指向问题
  • 二、垃圾回收机制:引用计数法的循环引用限制
  • 闭包
  • 例子1:一个简单的闭包
  • 解决方案
  • 例子2:包含事件处理程序的闭包
  • 解决方案
  • 如何实时查看内存占用
  • Chrome开发工具查看


什么是内存泄漏

js具有垃圾自动回收机制(GC)的语言。应用程序不再需要占用内存的时候,由于某些原因,本应被回收的内存没有被操作系统或可用的回收池回收,这时就发生了内存泄漏。

导致内存泄漏的原因

一、意外地创建全局变量

我们知道,全局变量属于根,它们始终被存储在内存中,不会被回收。因此,当因为代码书写不规范导致意外地创建全局变量时,内存中就会存储我们不希望存储的值。

例子1:没有使用var

function f() {
	a = 1;
	console.log(window.a == a);	//true
}

在这里,函数内部的变量a没有使用var声明,因此即使在函数作用域中,它依然属于全局变量。此时我们就存储了我们并不希望存储的值a。

例子2:this指向问题

(function f() {
	this.a = "value";	// this -> window
	console.log(window.a == a);		// true
})();

我们知道,在这个函数中,this指向调用 f 的对象,在这里是全局对象,因此a成为了全局变量而无法被回收。

二、垃圾回收机制:引用计数法的循环引用限制

IE9之前的版本中,其DOM和BOM对象是使用C++以COM(Component Object Model)对象的形式实现的,而COM对象的垃圾收集机制采用的是引用计数策略。因此,只要在IE中涉及COM对象,就会出现循环引用的问题。循环引用会导致内存无法被回收,因而出现内存泄漏。(具体参考关于js的垃圾回收机制)

下面看几个特殊场景下例子:

闭包

在IE9之前的版本中,如果闭包的作用域链中保存着一个DOM元素,那么该元素就无法被销毁。

例子1:一个简单的闭包
function createClosure() {
	var ele = document.getElementById("myDiv");
	var innerFun = function() {
		console.log(ele.id);
	}
}
setInterval(createClosure, 1000);

在本例中,由于内部函数innerFun中包含对父级作用域中ele的引用,ele的引用次数始终不为0,因此始终留在内存中而无法被回收。而当这段代码反复执行时,内存占用会不断上升,造成严重的内存泄漏。

解决方案

ele.id的一个副本保存在变量中,消除循环引用,最后添加ele=null解除对DOM对象的引用。

这里要注意的是,闭包会引用包含函数的整个活动对象,即使闭包不直接引用包含函数的对象,包含函数的活动对象中也依然会保存一个引用。因此将ele置为null是必要的。

function createClosure() {
	var ele = document.getElementById("myDiv");
	var id = ele.id;
	var innerFun = function() {
		console.log(id);
	}
	ele = null;
}
setInterval(createClosure, 1000);
例子2:包含事件处理程序的闭包

此例来自《高程3》。

<div id="myDiv">
  <input type="button" value="Click Me" id="myBtn">
</div>
<script type="text/javascript">
  var btn = document.getElementById("myBtn");
  btn.onclick = function() {
    document.getElementById("myDiv").innerHTML = "Processing...";
  }
</script>

这段代码是为了防止用户双击按钮,在用户点击一次后移除按钮,原来按钮的位置变成Processing...文本。
但问题是,当按钮被从页面中移除时,它依然带着一个事件处理程序onclick。即使按钮从页面中被移除了,但它与事件处理程序之间的引用依然存在。因此在某些浏览器(比如IE),它们很可能将元素和对事件处理程序的引用都保存在内存中。此时就发生了内存泄漏。

解决方案

针对上面的问题,有两种解决方案:

  1. 手动移除事件处理程序
btn.onclick = function() {
	btn.onclick = null; // 移除事件处理程序
	document.getElementById("myDiv").innerHTML = "Processing...";
  }
  1. 使用事件委托,将事件处理程序直接加在div上面。在点击按钮时,由于冒泡,依然会执行事件处理程序。(关于事件冒泡可以看关于事件冒泡与捕获
var myDiv = document.getElementById("myDiv");
myDiv.onclick = function() {
    document.getElementById("myDiv").innerHTML = "Processing...";
  }

如何实时查看内存占用

Chrome开发工具查看

以下面这段代码为例:

<!DOCTYPE html>
<html>
<head>
    <title>js内存泄漏</title>
</head>
<body>
    <div id="myDiv"></div>
    <script type="text/javascript">
        function createClosure() {
            var ele = document.getElementById("myDiv");
            var innerFun = function() {
                console.log(ele.id);
            }
        }
        setInterval(createClosure, 1000);
    </script>
</body>
</html>

在Chrome中打开网页,f12打开开发者工具,选择Performance > Memory,开始录制,6秒后停止录制,得到下面这幅图:

ScriptEngineManager 内存泄漏 javascript 内存泄漏_javascript


可以看到,js的内存占用稳定上升,很明显发生了内存泄漏。

然后再看我们优化后的代码:

<!DOCTYPE html>
<html>
<head>
    <title>js内存泄漏</title>
</head>
<body>
    <div id="myDiv"></div>
    <script type="text/javascript">
        function createClosure() {
            var ele = document.getElementById("myDiv");
            var id = ele.id;
            var innerFun = function() {
                console.log(id);
            }
            ele = null;
        }
        setInterval(createClosure, 1000);
    </script>
</body>
</html>

同样录制6s,得到下面这幅图:

ScriptEngineManager 内存泄漏 javascript 内存泄漏_全局变量_02


可以看到,js heap从高处下降,因为我们在代码的最后解除了对ele的引用。内存泄漏解决。