什么是垃圾回收机制?
和java,c#一样,javascript也有垃圾回收的机制,比如说c++和c就没有垃圾回收机制。可能有这么一种倾向,垃圾回收机制必须有一种平台来进行回收。比如说下面将讲的javascript的执行环境V8就会负责管理代码执行过程中的垃圾回收。
javascript具有自动垃圾回收机制,执行环境会负责管理代码执行过程中使用的内存。原理就是找出那些不再继续使用的变量,然后释放其占有内存。这整个过程也会按照一个固定的事件周期性的整形(时时的话开销太大)。
垃圾回收机制回收的是什么?
刚刚原理中提到要找出不再使用的变量,什么是不再使用的对象呢?不再使用的变量也就是生命周期结束的变量。目前javascript有两种变量,全局变量和在函数中产生的局部变量(暂不考虑ES6中的块级作用域)。
全局变量的声明周期一直持续到浏览器关闭页面才会清除,而局部变量只是在函数执行时存在,而在这个过程中会为局部变量在栈或者堆上分配相应的空间,来存储他们的值,然后当函数要使用这些变量的值时再取出来使用。一直到函数结束(闭包会不同)。
一旦函数结束,局部变量就不需要了,这时候就可以释放他们的内存。
var global = "I'm global";
function test(){
var local = “I m local” ;
}
test();
这个例子里面,global在关闭浏览器时释放,local在函数test结束后,释放。
什么时候触发垃圾回收
垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量,4096个对象,64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。
微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作智能了很多。
js的两种回收机制
标记清除(mark and sweep)
从语义上理解就比较好理解了,就是当变量进入到某个环境中的时候就把这个变量标记一下,比如标记为“进入环境”,当离开的时候就把这个变量的标记给清除掉,比如是“离开环境”,而在这后面还有标记的变量将被视为准备删除的变量。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上的标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作。销毁那些带标记的值并回收它们所占用的内存空间。
这是javascript最常见的垃圾回收方式。至于上面有说到的标记,到底该如何标记?好像是有很多方法,比如特殊位翻转,维护一个列表什么的。
工作原理:是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
- 去掉环境中的变量以及被环境中的变量引用的变量的标记。
- 再被加上标记的会被视为准备删除的变量。
- 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
引用计数(reference counting)
引用计数的含义是跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个时候的引用类型的值就会是引用次数+1了。如果同一个值又被赋给另外一个变量,则该值的引用次数又+1。
相反如果包含这个值的引用的变量又取得另外一个值,即被重新赋了值,那么这个值的引用就减一。当这个值的引用次数变成0时,表示没有用到这个值,这个值也无法访问,因此环境就会回收这个值所占用的内存空间。这样,当垃圾收集器下次再运行时,它就会释放引用次数为0的值所占用的内存。
但是刚刚也说了,第一种标记清除是最经常用到的,那么这个看起来这么好的引用计数为啥不被别人用了呢?
工作原理:跟踪记录每个值被引用的次数。
工作流程:
- 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是1。
- 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1.
- 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减1.
- 当引用次数变成0时,说明没办法访问这个值了。
- 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。
循环引用时的问题
但是循环引用的时候就会释放不掉内存。循环引用就是对象A中包含另一个指向对象B的指针,B中也包含一个指向A的引用。因为IE中的BOM、DOM的实现使用了COM,而COM对象使用的垃圾收集机制是引用计数策略。所以会存在循环引用的问题。
解决:手工断开js对象和DOM之间的链接。赋值为null。IE9把DOM和BOM转换成真正的JS对象了,所以避免了这个问题因为这个过程中会出现一个循环引用的问题!简单点来说就是一个对象小a的属性,引用了对象小b。小b对象也有一个属性引用了小a,那么小a,小b互相引用对方,也就造成了循环引用的问题啦。举个栗子:
function test(){
var a = {};
var b = {};
a.property = b;
b.property = a;
}
这就是一个很明显的循环引用了,小a和小b通过各自的属性互相引用,导致了内存无法释放。(有那么一点点的感觉像死锁。。。。)即使是再test()执行完后,如果使用标记清除是没有问题的,离开环境的时候就会被清除。但是引用计数不行,因为这两个对象的引用次数还是存在,不会变成0,所以其占用空间也不会清理,如果这个函数被调用多次,就会不断有内存被占用。造成了内存泄露。
IE中有一部分对象并不是原生JavaScript对象。例如,其BOM和DOM中的对象就是使用C++以COM(Component Object Model)对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。因此即使IE的js引擎是用的标记清除来实现的,但是js访问COM对象如BOM,DOM还是基于引用计数的策略的,也就是说只要在IE中涉及到COM对象,也就会存在循环引用的问题。比如说第一种情况:一个DOM元素和一个原生的js对象之间的循环引用
var ele = document.getElementById("ele");
var obj = {};
obj.property = ele;
ele.property = obj; //这种情况应该手动设置,在不适用的时候手工断开js和dom元素之间的连接
obj.property = null;
ele.property = null;
比如第二种情况是闭包的作用域链中包含着一个html元素,那么这个元素无法被销毁
window.onload = function outerFunction(){
var ele= document.getElementById("element");
ele.onclick = function (){
console.log(ele.id);
}
}
上面这个代码创建了一个作为ele元素处理程序的闭包,而这个闭包则又创建了一个循环引用。匿名函数中保存了一个outerFunction()的活动对象的引用,因此就会导致无法减少ele的引用。可以改成下面这个:
window.onload = function outerFunction(){
var ele= document.getElementById("element");
var id = ele.id;
ele.onclick = function (){
console.log(id);
}
ele = null;
}
在上面的代码中,通过把ele.id 的一个副本保存在一个变量中,并且在闭包中引用改变量消除了循环引用。
必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着elem。即使闭包不直接引用ele(比如上面的例子我们不用id),包含函数的多动对象中也依旧会保存一个引用。因此,有必要把ele变量设置为null。这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
垃圾回收机制的好处和坏处
好处:大幅简化程序的内存管理代码,减轻程序猿负担,并且减少因为长时间运转而带来的内存泄露问题。
坏处:自动回收意味着程序猿无法掌控内存。ECMAScript中没有暴露垃圾回收的借口,我们无法强迫其进行垃圾回收,更加无法干预内存管理。