前端面试-垃圾回收机制代码分析总结

348 阅读4分钟

通俗易懂的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变量会一直留在内存中 不会被清除,也可以利用它去做无法访问的变量 比如游戏中的生命值.
// 优点和缺点都是不会被清除 所以好好用闭包就可以了