通俗易懂的javascript垃圾回收机制
为什么要有垃圾回收?
如果住的屋子里有垃圾,不清理总有一天会堆满的,堆满的时候就没办法住了,程序也是如此,声明的变量和函数不清理的话,就会一直停留在内存中,内存快要被占满的时候电脑速度就会变慢,因此需要有垃圾回收机制。
JavaScript是一个带有自动垃圾回收的语言,与C语言手动创建内存空间malloc和free不同,js会周期性的运行垃圾回收程序。
常用的垃圾回收方法是标记清理和引用计数。
标记清理
JavaScript 最常用的垃圾回收策略是标记清理。顾名思义,这就是给变量加一个特殊的标记用来标记这个变量是不是没有用了。给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下 文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。
let a=1; //加标记
let b=2;//加标记
let c=3;//加标记
console.log(a,b) //程序在这里结束 去掉a b的标记,把c从内存中去掉
为什么清除c 而不是a,b? 因为它没有被用,没有用的变量在程序里会被清除,这就是标记清理。
引用计数
引用计数相对于标记清理不常用,因为它有缺点。引用计数的机制官方概念是:
对每个值都记录它被 引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。
let a={}; //a所指的对象引用值为1
let b={}; //b所指的对象引用值为1
let c={}; //c所指的对象引用值为1
a.d = 1; //a所指的对象引用值为2
a.d = b; //原本的值1被覆盖,a所指的对象的引用值减1,现在a所指的对象的引用值为1
a = c; //原本的值{}被覆盖,a所指的对象的引用值减1,现在a所指的对象引用值为0. 被垃圾机制回收
这种方式有问题!
function a(){
let a = {};
let b = {};
a.b = b;
b.a = a;
//a,b的引用值都为2,这是循环引用问题.这种没有作用的代码却占用了内存.
}
a();
为避免类似的循环引用问题,可以使用null来切断循环引用的联系;
a.b = null;
b.a = null;
//把变量设置为 null 实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。
使用null不仅可以切断引用,还可以释放内存。如果有对象很大但是没有用就可以使用null了。
(补充)内存泄漏
不经意的全局变量造成泄漏
function setName() {
name = 'Jake';
}
只要在变量声明前头加上 var、let 或 const 关键字即可,这样变量就会在函数执行完毕后离 开作用域。
定时器的内存泄漏
let bigbigbigobj = {};//很大的对象
setInterval(() => {
console.log(obj.name);
}, 100);
//只要定时器一直运行,回调函数中引用的 name 就会一直占用内存。垃圾回收程序当然知道这一点,因而就不会清理外部变量。
闭包的内存泄漏
let outer = function() {
let name = 'Jake';
return function() {
return name;
};
};
let a = outer(); //outer里面的name变量会一直留在内存中 不会被清除,也可以利用它去做无法访问的变量 比如游戏中的生命值.
// 优点和缺点都是不会被清除 所以好好用闭包就可以了