聊聊Javascript 垃圾回收机制-(一)
引子
我忽然想起来了还有个博客账号,最近工作任务较少,所以间歇性踌躇满志又更新一下-\_-
正文
背景介绍-什么是垃圾回收
垃圾回收(garbage-collection)是指对已分配的内存进行回收,当我们在创建一些变量,函数,对象时,例如:
var a = "Hello";
var getName = function (){ //函数体}
都需要分配内存,而当这些值不再被使用的时候,js就需要在合适的时候将这部分的内存进行回收,这就是垃圾回收机制,对于一些大型应用程序来说,垃圾回收可以有效提高性能。在js里,执行垃圾回收是自动执行的,不对外提供任何接口,不过还是有必要适当了解下它的原理。
回收条件-什么情况可以进行回收
了解完垃圾回收的概念以后,首要的问题就来了:对于已分配出去的内存,什么情况下可以进行垃圾回收呢?
联系下实际生活:我们会把什么样的东西送去回收呢,那当然是确定以后根本用不到的东西。
在js里,也是一样,对于再也无法访问到的值,我们就要进行回收。
一般案例
举个简单例子:
var user = {
name: 'Leo'
}; // 第一步,创建一个对象并把内存地址赋值给user
user = null; // 第二步,修改user的内存地址
之前在其他文章已经说过,创建引用类型值的时候,赋值给变量的实质上是内存地址。执行上述代码的第一步之后,可以通过window.user访问到{name: 'Leo'}对象,
但是在执行user = null之后,这个变量对象其实已经无法访问到了。也就达到了可以被回收的条件。再看个稍微复杂的案例:
var user = {
name: 'Leo',
friend: {
name: 'John'
}
};
// 接下来移除引用
user.friend = null;// 第一种情况,修改user.friend的地址
user = null;// 第二种情况
这个例子中比前面多增加了一个对象{name: 'John'}, 移除引用前后对应的图如下:互相引用
现在大家应该对什么叫做无法被访问有一个大概的概念了。 那么来个特殊一点的:
// '朋友圈'函数 让传入的两个人成为朋友,并返回这个小圈子
function circleOfFriends(user1, user2){
user1.friend = user2;
user2.friend = user1;
return {
user1,
user2
}
}
var circle = circleOfFriends({name: 'Leo'},{name: 'John'});
执行这个之后,关系图如下:
window可以通过circle访问到circle对象,circle可以通过user1`user2属性访问到{name: 'Leo'},{name: 'John'}`两个对象, 所以此时这三个对象都还是可访问的。
如果接下来依次执行以下步骤:
circle.user1 = null; //此时如果要访问{name: 'Leo'}对象 还可以通过circle.user2.friend 来实现,所以它依然是可以访问到的
接着执行:
circle.user2.friend = null; // 此时{name: 'Leo'}被彻底孤立,再也不可访问,它满足可以被回收的条件
注意,从图中可以看到{name: 'Leo'}还有引用其他对象, 但是它自身是通过任何方式都无法被访问到的。所以此时它还是属于可回收的。
当然,如果我们不执行上面的语句,而是直接执行:
circle = null;
此时,circle指向的对象,{name: 'Leo'}对象,{name: 'John'}对象,都将变得不可访问,即使他们之间内部是有互相引用关系。
到这里,我们似乎就可以归纳出,满足可被回收的条件:
从全局对象出发,只要不能够直接或者间接被访问到的值,就满足可回收的条件;
真的仅仅是这样吗? 我们似乎还遗漏了一个重要的地方-- 执行环境。 曾几何时,我们在介绍闭包的时候讲过执行环境和作用域链, 很显然的,当前执行环境和作用域链上的值 也属于可访问的,例如最简单的函数例子:
var b = 2;
function getA(){
var a = 1;
return a ;
}
getA();
当运行到getA()之前时,变量b是可访问的,但是变量a是不可访问的, 当执行getA函数内部时, 变量a就是可以访问的了,当然此时变量b也可以访问。
所以我们需要完善一下上面的结论:
从当前执行环境以及作用域链上可访问的对象出发,任何可以被访问的对象,都算是可访问对象。
当然 如果认为执行环境也是当前全局对象可访问的对象之一,那一样可以用前面的简化版结论,表述方式没必要太较真,重点是要记住执行环境和作用域链这个要点。
(有印象的同学不妨思考下很久之前说过的闭包,思考下闭包和垃圾回收机制的关系,后面有机会也会继续补充说明)
垃圾回收的算法
标记清除法
其实上面的内容讲完, 标记清除法的思路,也就基本清楚了。 标记清除法,顾名思义分为2个步骤:
- 标记:从根节点(这里的根节点,指的就是上文提到的全局对象以及当前执行环境以及作用域链上可访问的对象)出发,深度优先遍历所有可以访问到的节点,并打上"可访问"的标记
- 清除:从所有节点里,清除掉没有打上标记的节点。
这里介绍的是核心思路,具体实践的时候有些地方是可以优化的,例如第一步标记的过程,对于某些节点有可能被重复遍历的,就像前面提到的'朋友圈'模型中,既可以通过circle访问其中的某个对象, 也可以通过某个user的friend属性访问到, 那么为了避免重复遍历,我们就可以另外用一种标记,来标识表示节点已经被遍历过,详细算法会在后续的系列文章介绍。
引用计数法
其实mdn还介绍了早期的另一种回收算法,我们也介绍提一下。
引用计数法顾名思义是就是先记下某个变量被引用的总次数,然后当被引用次数为0时,就表示可回收。
这个思路咋一看和标记清除法是很相似的,但是实际上有个很明显的区别,就是在前面互相引用的例子中:两个user其实都是不可获得的,但是由于互相引用,它们的被引用次数并不为0,那么按照引用计数法,这两个对象就不会被清除,这是有问题的, 所以从2012年起,所有的现代浏览器都是使用标记清除法。
总结
文本主要介绍一下js中垃圾回收机制和两种垃圾回收算法,希望大家看完后对此有个简单的概念,其中比较重要的是辨析各种满足垃圾回收机制的情景,还有就是对象有被引用不等于可访问(互相引用)关于垃圾回收机制其实还有蛮多的问题需要探索,例如:
- 标记清除法的具体实现
- 垃圾回收执行的时机和频率是怎么样的
- 有哪些优化机制保证清除效率
会在此系列的后续文章上进一步更新说明。(应该会吧~)
惯例:如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流
然后还有一些想说的就是,很感谢大家的关注和一些私信,尤其是看到一些读者说看了文章以后确实有帮助的,会觉得很感动。 其实自己也满惭愧的,每次更新都间很久,确实有点太懒了~, 以后会尽量勤快一些。