Javascript的垃圾回收和内存泄漏
阅读本章节前,需要读者理解执行上下文和作用域,作者有相关文章供参考。
程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。
对于持续运行的服务进程,必须及时释放不再用到的内存(垃圾回收)。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏。
垃圾回收
垃圾回收指的是一种自动内存管理机制,垃圾收集器会定期(周期性)找出那些不再继续使用的变量、对象等,释放占用的内存,从而避免内存泄漏。但是这个过程不是实时的,因为其开销比较大并且垃圾回收时会停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
JavaScript引擎通过标记清除(Mark-and-Sweep)算法来实现垃圾回收。当某个(局部变量,因为全局变量要等页面被销毁)变量不再被引用(生命周期结束)时,垃圾回收器会将其标记为可回收状态。当发生内存不足时,垃圾回收器会扫描内存中的所有对象,将未被标记的对象视为垃圾并回收。
** 示例:**
function fn1() {
var obj = {name: 'Tom', age: 18};
}
function fn2() {
var obj = {name:'Sam', age: 20};
return obj;
}
var a = fn1(); // undefined
var b = fn2(); // b = obj = {name:'zhangsan', age: 10};
在上面的代码中,我们首先声明了两个函数,分别叫做 fn1 和 fn2。
当 fn1 被调用时,进入 fn1 的执行上下文,会开辟一块内存存放对象 {name: 'Tom', age: 18}。而当调用结束后, fn1 的执行上下文出执行栈,那么该块内存会被JavaScript引擎中的垃圾回收器自动释放;
在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。fn2执行上下文中保存的变量被保存在了全局上下文中
那么javascript中如何确定变量是没有用的
垃圾收集器会跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除和引用计数,目前主要使用标记清除。
标记清除
JavaScript 中最常用的垃圾回收方式就是标记清除。
当变量进入环境时,例如,在函数中声明一个变量(结合执行上下文理解),就将这个变量标记为进入环境。
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
而当变量离开环境时,则将其标记为离开环境。
function test(){
// 调用函数,创建执行上下文,记录变量a,b
var a = 10 ; // 被标记
var b = 20 ; // 被标记
}
test(); // 执行完毕,a、b 又被标记离开,被回收。
最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
到目前为止,IE9+、Firefox、Opera、Chrome、Safari 的 JS 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。
相反,如果包含对这个值引用的变量又被赋予了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
function test() {
var a = {}; // a 指向对象的引用次数为 1
var b = a; // a 指向对象的引用次数加 1,为 2
var c = a; // a 指向对象的引用次数再加 1,为 3
var b = {}; // a 指向对象的引用次数减 1,为 2
}
Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用。
循环引用指的是对象 A 中包含一个指向对象B的指针,而对象 B 中也包含一个指向对象 A 的引用。
function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
fn();
以上代码 a 和 b 的引用次数都是 2,fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和 b 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露。在 IE7 与 IE8 上,内存直线上升。
闭包
要理解 JavaScript 中的闭包,非常容易,但是在此之前你需要先知道以下两个知识点:
- JavaScript 中的作用域和作用域链
- JavaScript 中的垃圾回收
作用域和作用域链
作用域就是一个独立的地盘,让变量不会外泄、暴露出去,不同作用域下同名变量不会有冲突。
作用域在定义时就确定,并且不会改变。
如果在当前作用域中没有查到值,就会向外层作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
垃圾回收
Javascript 执行环境会负责管理代码执行过程中使用的内存,其中就涉及到一个垃圾回收机制
垃圾收集器会定期(周期性)找出那些不再继续使用的变量,只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的变量等信息可以在函数中使用。换句话说,执行函数时,只要在函数作用域中使用了外层作用域的数据,就创建了闭包。而作用域链,正是实现闭包的手段。
下面通过两段代码来演示:
function test() {
var name = 'Tom';
console.log(name); // 断点调试
}
test();
在上面的代码中,我们在函数test中定义了一个变量name,然后打印这个name 变量。对于 name 这个函数来讲,自己的函数作用域中存在这个变量,所以我们在调试时可以看到 Local 中存在变量 。
var name = 'Tom';
function test() {
console.log(name); // 断点调试
}
test();
在上面的代码中,我们将声明 name 这个变量的动作放到了 test 函数外面,也就是说 test 函数在自己的作用域已经找不到这个 name 变量了,它会顺着作用域链一层一层往外找。然而上面在介绍闭包时说过,如果出现了这种情况,也就是函数使用了外部的数据的情况,就会创建闭包。
仔细观察调试区域,我们会发现此时的 name 就放在 Closure 里面的,从而证实了我们前面的说法。但是这样一来,是不是多数场景都会造成闭包现象,占用很多内存空间呢?
var global = 'global';
function out() {
var outFunc1 = 'outFunc1';
var outFunc2 = 'outFunc2';
function between() {
var between1 = 'between1';
var between2 = 'between2';
function test() {
console.log(global, outFunc1, between1); // 断点调试1
}
test();
}
between();
}
out();
console.log(global, outFunc1, between1); // 断点调试2
在上面的代码中,函数 test 中一个变量都没有创建,却要打印 global, outFunc1, between1 ,这些变量分别存在于 out和between 函数以及全局作用域中,因此创建了 3 个闭包,全局闭包里面存储了 global 的值,闭包 out 中存储了变量 outFunc1 的值,闭包 between 中存储了变量 between1 的值。outFunc2和between2未被引用,所以不被储存到闭包中。
断点调试1结果
断点调试2结果
在最后的console输出时,断点调试就可以清楚的看到,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。
在test函数执行时,引用了外部变量创建闭包。当整个test执行完毕,且没有任何结果返回,执行上下文被销毁函数也就被垃圾回收,其引用的变量失去了引用,最后也被回收,所以不会有任何内存占用的情况。————这种现象称为:自动形成的闭包
什么时候会函数执行完毕引用的变量未被销毁呢?
function test() {
var name = 'Tom';
return function () {
console.log(name);
};
}
var out = test();
out(); // Tom
在这个例子中,test 函数返回一个函数,并在这个内部函数中访问 name 这个局部变量。调用 test 函数并将匿名函数赋给 out 变量,此时 out 指向的函数内部包含了对name的引用,每次调用 out 都会引用到 name变量,导致 name 无法被垃圾回收。
至此,闭包的一个优点或者特点也就体现出来了,那就是:
- 通过闭包可以让外部环境访问到函数内部的局部变量。
- 通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁。
总结
-
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
-
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
-
我们还可以通过一些手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。