我忽然想起来了还有个博客账号,最近工作任务较少,所以间歇性踌躇满志又更新一下-\_-
垃圾回收(garbage-collection)是指对已分配的内存进行回收,当我们在创建一些变量,函数,对象时,例如:
var a = "Hello"; var getName = function (){ //函数体}
都需要分配内存,而当这些值不再被使用的时候,js就需要在合适的时候将这部分的内存进行回收,这就是垃圾回收机制,对于一些大型应用程序来说,垃圾回收可以有效提高性能。在js里,执行垃圾回收是自动执行的,不对外提供任何接口,不过还是有必要适当了解下它的原理。
了解完垃圾回收的概念以后,首要的问题就来了:对于已分配出去的内存,什么情况下可以进行垃圾回收呢?
联系下实际生活:我们会把什么样的东西送去回收呢,那当然是确定以后根本用不到的东西。
在js里,也是一样,对于再也无法访问到的值,我们就要进行回收。
举个简单例子:
var user = { name: 'Leo' }; // 第一步,创建一个对象并把内存地址赋值给user user = null; // 第二步,修改user的内存地址
之前在其他文章已经说过,创建引用类型值的时候,赋值给变量的实质上是内存地址。执行上述代码的第一步之后,可以通过window.user
访问到{name: 'Leo'}
对象,
但是在执行Leo = 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中垃圾回收机制和两种垃圾回收算法,希望大家看完后对此有个简单的概念,其中比较重要的是辨析各种满足垃圾回收机制的情景,还有就是对象有被引用不等于可访问(互相引用)关于垃圾回收机制其实还有蛮多的问题需要探索,例如:
会在此系列的后续文章上进一步更新说明。(应该会吧~)
惯例:如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流,主页有邮箱地址
然后还有一些想说的就是,很感谢大家的关注和一些私信,尤其是看到一些读者说看了文章以后确实有帮助的,会觉得很感动。 其实自己也满惭愧的,每次更新都间很久,确实有点太懒了~, 以后会尽量勤快一些。
顺便再说下,RingCentral目前在杭州也设置了办公点,而且可以申请长期远程办公,帮你告别996,工作生活两不误,有兴趣的同学可以私信或者发邮件给我,可以免费帮忙内推~
http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection
https://javascript.info/garbage-collection
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory\_Management