今天我们来聊一聊js中的垃圾回收机制,小编将会从两个方面带大家去了解js中的垃圾回收机制,分别是:
- 为什么需要垃圾回收机制
- 垃圾回收机制的实现方式是什么
为什么需要垃圾回收机制
顾名思义,垃圾回收就是把垃圾给回收起来(听君一席话,如听一席话,哈哈哈),其实对应到js当中就是把不再使用的变量所占用的内存给释放掉,至于为什么需要这样,这就得说一下内存泄漏了:
什么是内存泄漏
我们知道,一个程序的运行是需要操作系统给它分配相应的内存才能够运行起来的,对于持续运行中的程序,它所占用的内存很有可能会越来越大,如果不能够去及时释放不再需要的内存的话,内存占用越来越高,会导致程序性能低下,更严重的还可能会引起整个进程的崩溃。总的来说,如果不能够及时释放掉不再需要的内存,这就叫内存泄漏
那么话说回来,js垃圾回收机制的存在就是为了防止出现内存泄漏的问题的,对于不再使用的变量,js会定期的释放掉这些变量所占用的内存,从而保证我们的程序能够稳定的运行下去。
既然垃圾回收机制如此的重要,那么相信屏幕前的你也一定十分想知道它究竟是如何工作的,请接着往下看:
垃圾回收机制的实现方式
我们已经知道,js具有自动的垃圾回收机制,释放掉那些不再需要的变量,但这个过程其实并不是实时的,而是按照固定的间隔时间去周期性的执行,那么这时会有一个新的问题出现:
到底什么才是不再需要的变量?
示例代码:
function test01() {
var obj1 = {name: 'author', age: 18};
}
function test02() {
var obj2 = {name:'zero', age: 18};
return obj2;
}
var t1 = test01(); // obj1变量内存被释放
var t2 = test02(); // obj2变量内存保留
在上面的代码中我们可以看到,调用test01时,会给obj1开辟一块内存,当test01执行完毕之后obj1占用的内存就被释放了,但是obj2变量的内存却被保留了下来,原因是因为obj2返回被外部的变量t2所接收,也就是说obj2被引用了,所以它的内存被保留了下来。那么这时就会有一个新的疑问产生了:垃圾回收机制是怎么知道变量是否被引用呢?
通常情况下,垃圾回收机制的实现方式会有两种:
- 标记清除 (比较常用)
- 引用计数 (不常用)
下面我们就来看看这两种方式是如何运作的
标记清除
js比较常用的清除方式就是标记清除,当进入变量所处的环境时相关变量会被标记上进入环境,当离开相关的环境时,变量又会被标记上离开环境,而这些被标记为离开的就是会在下一次垃圾回收机制运行时所要被清除的变量,听着有点抽象,我们直接看代码:
function test(){
var a = 10 ; // 被标记 ,进入环境
}
test(); // 函数执行完之后 a被标离开环境,被回收。
当然,这是很符合我们所认知的逻辑的,从逻辑上讲,被标记为进入环境的变量的内存是永远不能被释放的,因为很有可能我们还会用到,然后函数执行完毕,不再需要了则可以释放掉,但是函数执行完毕之后真的所有变量都会被释放吗?其实不然,这其中还有一直特殊的情况——闭包
function debounce(callback, duration) {
let timeId = null; // 标记为进入环境
return function (...args) {
if (timeId) {
clearTimeout(timeId);
}
timeId = setTimeout(function () {
callback(...args);
}, duration);
};
}
// debounce运行完毕,timeId标记为离开环境
const fn = debounce(() => {console.log("Hello World!")}, 1000);
相信这段代码大家也都很熟悉,这是一个很经典的防抖函数,这里面就用到了闭包,按照我们上文所说,debounce函数运行完之后,timeId的变量就应该被清除了,但用过防抖函数的朋友应该都知道这里timeId实际上并没有被清除,其实当函数执行完毕之后,它还会去掉当前所在环境中的变量以及被该变量所引用的变量的标记
简单解释一下:
- debounce函数运行完之后,当前所处的环境是全局作用域,该作用域下面有变量fn
- fn的值是debounce函数返回的匿名函数,而该匿名函数中又使用了变量timeId
- 就相当于是fn有着对timeId的间接引用,所以timeId被打上的相关标记被清除,故不会被垃圾回收机制回收
(相信以在座的各位的理解能力应该都能明白我在说什么吧,——手动滑稽)
最后,垃圾回收机制会清理仍然被打上标记的变量,完成内存释放的工作,这就是标记清除的实现方式
其实现在来说,包括IE9+、Firefox、Chrome等主流浏览器的JS垃圾回收所使用的都是标记清除的方式或者是类似的方式,只不过回收的周期有所不同
说完了标记清除,我们再来了解一下引用计数的方式:
引用计数
顾名思义,引用计数的方式实际上也就是统计每个值被引用的次数,次数为0则被回收
话不多说,直接看代码:
function test() {
var a = {}; // a 指向对象的引用次数为 1
var b = a; // a 指向对象的引用次数加 1,为 2
var c = a; // a 指向对象的引用次数再加 1,为 3
b = {}; // a 指向对象的引用次数减1,为2; b所指向对象的引用次数为1
b = null; // b所指向对象的引用次数为0 该对象被回收
}
相信通过这段代码在座的小天才们都能看明白小编想说什么了,就无需多言了。
但这种方式会存在一个弊端:循环引用
循环引用是指有两个对象A、B,A对象的某个属性引用B,而B对象的某个属性又引用A,那么这就形成了——俄罗斯套娃! (啊呸) ——循环引用!
代码示例:
function test() {
var a = {}; // a 指向对象的引用次数为 1
var b = {}; // b 指向对象的引用次数为 1
a.obj = b; // b 指向对象的引用次数为 2
b.obj = a; // a 指向对象的引用次数为 2
}
test();
上述代码中a和b所指向对象的引用次数都是2,其实按照我们的理解来说,test函数执行完毕之后,a和b两个对象都已经不再需要,可以被释放内存的,如果此时是标记清除的方式的话则不会有问题,引用计数则会出现循环引用的现象,如果test函数被大量调用的话,内存的增长会十分严重,比如在IE7和IE8的环境中,内存用量直线飙升