前言
上一文在介绍堆内存和栈内存的时候,提到了垃圾回收机制。JavaScript具有自动垃圾回收机制,会定期堆那些我们不再使用的变量所占有的内存进行释放。
内存生命周期
- 分配你所需的内存:在JS中,不管是进行值的初始化,还是通过函数调用,都会自动帮其分配内存,如果是基本类型久存储在栈内存中,如果是引用类型,就将其存储在堆内存中,并将指向堆内存地址的指针存储在栈内存中;
- 使用分配到的内存:当我们要对变量进行读取或者写入的时候,就去内存中查找(修改)他们的值;
- 不需要时将内存释放:JS的自动垃圾回收机制,会跟踪内存的分配和使用,以便变量不再被使用时,自动释放它。
垃圾回收机制
JS如何辨别那些值是可以回收的呢?这时候就需要了解一下垃圾回收算法了。垃圾回收算法主要分两种:
- 引用计数;
- 标记清除。
引用计数
主要是统计引用类型变量声明后被引用的次数,当次数为0时,这个变量将会被收回。
const o = {};
let a = o; // o被a引用,计数为1
let b = o; // o被b引用,计数为2
a = {}; // a不再引用o, 计数为1
b = null; // a不再引用o, 计数为0, 这时候o将被回收
但是引用计数有一个缺点,叫做循环引用。
const o1 = {};
const o2 = {};
o1.test = o2;
o2.test = o1;
o1、o2
互相引用,计数永远不可能为0,所以建议在不需要o1、o2
时,手动设置为null
,解除引用。
标记清除
当变量进入环境时,标记为“进入环境”,当变量离开环境时,将标记为“离开环境”,此时,这个变量将会被收回。具体做法如下:
- JS开始运行时,会给存储在内存中的所有变量加上标记;
- 然后去掉环境中的变量和被环境中的变量引用的变量的标记;
- 此后,再被加上标记的变量被视为准备删除的变量;
- 最后,垃圾收集器,销毁那些带有标记的值,并释放它们的内存空间。
function foo(){
const a = 'a';
const b = 'b';
}
foo();
当执行foo
时,建立执行上下文,此时a、b
进入环境;当执行完毕之后,a、b
离开环境,最后,a、b
会被垃圾收集器销毁,并释放它们的内存空间。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。
常见的内存泄露案例
当JS环境中存在不需要的内存,但是由于没有被释放,造成了内存泄漏。
全局变量
function foo(){
name = '王花花';
}
foo();
执行foo
时,由于name
前面缺少var
,先在当前作用域寻找name
,没找到,于是向上一级的全局作用域寻找,这里也没有,于是会自动在全局作用域进行声明,所以会把var
定义到全局作用域中,当foo
执行完毕,但name
这个全局变量,并不会被销毁,导致了name
内存泄漏。
闭包
function foo(){
var a = 'a';
function bar(){
console.log(a);
}
return bar;
}
const test = foo();
test(); // a
foo
虽然执行完毕了,但是将bar
返回给了test
,所以此时foo
内的变量还被引用着,所以造成了内存泄漏。
DOM引用
let el = document.getElementById('el');
el.text = '123';
document.body.removeChild(document.getElementById('el'));
console.log(el.text); // 123
即使我们对el
元素移除,但是el
变量仍旧保留对DOM的引用,所以造成了内存的泄漏。
总结
- JS的垃圾回收机制是定期自动执行的;
- 垃圾回收机制算法有标记清除和引用计数;
- 不合理的代码会造成内存泄漏。