[转][浏览器GC]深入理解JavaScript垃圾回收
理解浏览器 GC 机制不只可以满足面试的需要,更重要的是在 SPA 大行其道的时代,编写长时运行的程序时,对内存泄露问题的控制预防以及写出更高效流畅的浏览器程序都很有帮助,这篇文章讲的比较容易理解,不过新生代老生代区的标记清除,以及内存碎片优化之类的没有展开讲。想要更详细的了解可以看转的这篇:[转][浏览器GC]老大,不好了,内存泄漏了!
原文:juejin.cn/post/6930648689832624142
作者:SwordQiu
引言
《JavaScript 高级程序设计第四版》关于垃圾回收,我个人认为关于标记清除写的是云里雾里的,直到我看到这篇文章,这里上英文版和中文版连接:
如果大家有兴趣可以读原文,我在这里用我的话描述一下并增加一些扩展内容。
如有错误,感谢指正。🌟🌟
什么是垃圾
首先我们需要先知道什么是垃圾。
生活常识里什么是垃圾呢?
现在以及未来都不再被需要的物体就是垃圾。程序里也是一样的道理。
文章说了一个名词,叫做可达性。
如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的
也就是说你不再能够访问到它的数据,就是不可达的。
没有可达性的数据就可以被回收。
这个逻辑很简单暴力,你都访问不到它了,确实没有要它的必要啊。
毫无疑问,全局变量随时有可能被访问到,所以它是可达的,它不能是垃圾。
其次,正在执行的函数,比如说这样的
function fn(){
var b=2
console.log(b)
}
fn()
当函数执行的时候,b的数据就会被生成,此时它是可达性的,不能被删除。
然而当函数执行完后,它不能再被访问到,此时它是不可达的,需要删除。
那么现在结论就是:
全局环境下的变量因为都具有可达性,所以都不是垃圾不能被回收。
局部环境下的变量因为不再具有可达性,所以都是垃圾可以被回收。
但凡事都可以被改变。
引用转移
单个变量名引用转移
// user 具有对这个对象的引用
let user = {
name: "John"
};
上面这段代码在内存中是这样的
这是一个在全局环境下的变量,很有可能别的代码会用到它,所以不能被回收。
但是,当 user 这个变量名引用变了
user = null
那原来的{name:John}
这个数据就访问不到它了,此时就可以被回收。
但是如果代码是这样的
let user = {
name: "John"
};
let admin = user
user = null
此时{name:John}
依然可以通过变量admin
访问,它就不能被回收。
相互关联的对象
这里有一个更加复杂的对象
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
此时内存图是这样的
如果此时执行这样的代码
delete family.father;
delete family.mother.husband;
就会变成这样 此时
{name:John}
不再可达,那么它就会被回收。
那么根据内存图,如果把family
这个变量名引用到其他地方。
family = null;
很显然,原先的数据不再可达,它就会被回收。
垃圾回收的策略
上面是垃圾回收的基本概念,非常符合现实逻辑。
下面是关于策略
标记清理
定期执行以下“垃圾回收”步骤,一开始数据是这样的
1.垃圾收集器通过遍历找到所有的根,并“标记”(记住)它们。
2.如果还有引用的数据,说明是可达的。一直标记到最后一层引用。
3.此时发现有一个数据,从来没有被根(全局变量能访问到的)引用到,就把它回收掉。
当然遍历的同时,因为数据量很大,为了不影响 JS 代码运行,加快垃圾回收速度,就做了一些优化处理(包括但不限于)
- 分代收集
通俗化就是把数据分为两种,新生代和旧生代。有些函数中的变量可以很快被垃圾回收掉,而长期没有被回收的变量,会从新的变成旧的,而且有可能永远不会被删除,所以就降低用垃圾回收检查它们的频次。—-减少对某些数据定期遍历的次数
- 增量收集
慢慢收集,把垃圾回收的范围慢慢扩大而不是一次全部遍历完。—-减少收集的范围,提高遍历的量次
- 闲时收集
这个非常通俗,让JS
代码先走,闲下来的时候再去收集垃圾
引用计数(扩展)
这方面红宝书就写的比较清楚
另一种不常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。
声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0 的值的内存。
不过引用计数有非常大的问题:如果是互相引用,次数也会叠加
function fn(){
var a=1
var b=a
}
fn()
此时变量a
会被记成2次引用,即使函数执行完了,a
也不会被当成垃圾回收。
闭包(扩展)
注意到了吗,上面的例子没有讲到闭包,对于闭包我个人认为可以把它当成全局变量来看。
以下摘自阮一峰的博客
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
闭包的产生是因为一个函数被当前函数作用域外部的变量引用了,除非外部的变量被释放,否则闭包当然不会被回收。
下面是例子:
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();//此时 result 相当于拿到了
result(); // 999
nAdd();
result(); // 1000
上面的数据n
虽然在函数中,但是依然会存于内存中。
如果nAdd
跟result
的引用没有被释放,那么数据n
不会被垃圾回收。
总结
- 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
- 当对象是可达状态时,它一定是存在于内存中的。
- 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。
最后🌟🌟
以上就是我对于垃圾回收的简单分享。
如果您觉得对您有帮助,请顺手点个赞,谢谢!
完结撒花!🌸🌸🌸