1、垃圾回收的必要性javascript
下面这段话引自《JavaScript权威指南(第四版)》html
因为字符串、对象和数组没有固定大小,全部当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次建立字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们可以被再用,不然,JavaScript的解释器将会消耗完系统中全部可用的内存,形成系统崩溃。java
这段话解释了为何须要系统须要垃圾回收,JS不像C/C++,他有本身的一套垃圾回收机制(Garbage Collection)。JavaScript的解释器能够检测到什么时候程序再也不使用一个对象了,当他肯定了一个对象是无用的时候,他就知道再也不须要这个对象,能够把它所占用的内存释放掉了。例如:数组
var a = "before"; var b = "override a"; var a = b; //重写a
这段代码运行以后,“before”这个字符串失去了引用(以前是被a引用),系统检测到这个事实以后,就会释放该字符串的存储空间以便这些空间能够被再利用。浏览器
2、垃圾回收原理浅析app
如今各大浏览器一般用采用的垃圾回收有两种方法:标记清除、引用计数。ide
一、标记清除函数
这是javascript中最经常使用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,由于只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
垃圾收集器在运行的时候会给存储在内存中的全部变量都加上标记。而后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此以后再被加上标记的变量将被视为准备删除的变量,缘由是环境中的变量已经没法访问到这些变量了。最后。垃圾收集器完成内存清除工做,销毁那些带标记的值,并回收他们所占用的内存空间。性能
关于这一块,建议读读Tom大叔的几篇文章,关于做用域链的一些知识详解,读完差很少就知道了,哪些变量会被作标记。
二、引用计数优化
另外一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每一个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,若是包含对这个值引用的变量又取得了另一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,于是就能够将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
可是用这种方法存在着一个问题,下面来看看代码:
function problem() { var objA = new Object(); var objB = new Object(); objA.someOtherObject = objB; objB.anotherObject = objA; }
在这个例子中,objA和objB经过各自的属性相互引用;也就是说这两个对象的引用次数都是2。在采用引用计数的策略中,因为函数执行以后,这两个对象都离开了做用域,函数执行完成以后,objA和objB还将会继续存在,由于他们的引用次数永远不会是0。这样的相互引用若是说很大量的存在就会致使大量的内存泄露。
咱们知道,IE中有一部分对象并非原生JavaScript对象。例如,其BOM和DOM中的对象就是使用C++以COM(Component Object
Model,组件对象)对象的形式实现的,而COM对象的垃圾回收器就是采用的引用计数的策略。所以,即便IE的Javascript引擎使用标记清除的策略来实现的,但JavaScript访问的COM对象依然是基于引用计数的策略的。说白了,只要IE中涉及COM对象,就会存在循环引用的问题。看看下面的这个简单的例子:
var element = document.getElementById("some_element"); var myObj =new Object(); myObj.element = element; element.someObject = myObj;
上面这个例子中,在一个DOM元素(element)与一个原生JavaScript对象(myObj)之间创建了循环引用。其中,变量myObj有一个名为element的属性指向element;而变量element有一个名为someObject的属性回指到myObj。因为循环引用,即便将例子中的DOM从页面中移除,内存也永远不会回收。
不过上面的问题也不是不能解决,咱们能够手动切断他们的循环引用。
myObj.element = null; element.someObject =null;
这样写代码的话就能够解决循环引用的问题了,也就防止了内存泄露的问题。
3、减小JavaScript中的垃圾回收
首先,最明显的,new关键字就意味着一次内存分配,例如 new Foo()。最好的处理方法是:在初始化的时候新建对象,而后在后续过程当中尽可能多的重用这些建立好的对象。
另外还有如下三种内存分配表达式(可能不像new关键字那么明显了):
一、对象object优化
为了最大限度的实现对象的重用,应该像避使用new语句同样避免使用{}来新建对象。
{“foo”:”bar”}这种方式新建的带属性的对象,经常做为方法的返回值来使用,但是这将会致使过多的内存建立,所以最好的解决办法是:每一次函数调用完成以后,将须要返回的数据放入一个全局的对象中,并返回此全局对象。若是使用这种方式,就意味着每一次方法调用都会致使全局对象内容的修改,这有可能会致使错误的发生。所以,必定要对此全局对象的使用进行详细的注释和说明。
有一种方式可以保证对象(确保对象prototype上没有属性)的重复利用,那就是遍历此对象的全部属性,并逐个删除,最终将对象清理为一个空对象。
cr.wipe(obj)方法就是为此功能而生,代码以下:
// 删除obj对象的全部属性,高效的将obj转化为一个崭新的对象! cr.wipe = function (obj) { for (var p in obj) { if (obj.hasOwnProperty(p)) delete obj[p]; } };
有些时候,你可使用cr.wipe(obj)方法清理对象,再为obj添加新的属性,就能够达到重复利用对象的目的。虽然经过清空一个对象来获取“新对象”的作法,比简单的经过{}来建立对象要耗时一些,可是在实时性要求很高的代码中,这一点短暂的时间消耗,将会有效的减小垃圾堆积,而且最终避免垃圾回收暂停,这是很是值得的!
二、数组array优化
将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),可是须要注意的是,这种方式又建立了一个新的空对象,而且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,而且同时能实现数组重用,减小内存垃圾的产生。
三、方法function优化
方法通常都是在初始化的时候建立,而且此后不多在运行时进行动态内存分配,这就使得致使内存垃圾产生的方法,找起来就不是那么容易了。可是从另外一角度来讲,这更便于咱们寻找了,由于只要是动态建立方法的地方,就有可能产生内存垃圾。例如:将方法做为返回值,就是一个动态建立方法的实例。
在游戏的主循环中,setTimeout或requestAnimationFrame来调用一个成员方法是很常见的,例如:
setTimeout( (function(self) { return function () { self.tick(); }; })(this), 16)
每过16毫秒调用一次this.tick(),嗯,乍一看彷佛没什么问题,可是仔细一琢磨,每一次调用都返回了一个新的方法对象,这就致使了大量的方法对象垃圾!
为了解决这个问题,能够将做为返回值的方法保存起来,例如:
// at startup this.tickFunc = ( function(self) { return function() { self.tick(); }; } )(this); // in the tick() function setTimeout(this.tickFunc, 16);
相比于每次都新建一个方法对象,这种方式在每一帧当中重用了相同的方法对象。这种方式的优点是显而易见的,而这种思想也能够应用在任何以方法为返回值或者在运行时建立方法的状况当中。
四、高级技术
从根本上来讲,javascript自己就是围绕着垃圾收集来设计的。随着咱们工做的进行,避免内存垃圾变得愈来愈困难。由于不少方便实用的Javascript库方法也会产生一些新的对象。对于这些库方法产生的垃圾,咱们一筹莫展,只能从新翻看文档,而且检查方法的返回值。例如,数组的slice方法返回一个新的数组(在不修改原数组的基础上,截取出一部分做为新数组),字符串的substr方法返回一个新的字符串(在不修改原字符串的基础上,截取出一部分字符串做为返回值)等等。
调用这些库方法,将会建立内存垃圾,而你能作的,只有避免调用这些方法,或者用不建立系统垃圾的方式重写这些方法(有点极端啦~)。
例如,在Construct 2引擎中,从数组中利用下标来删除一个元素,是常常进行的操做。最初咱们是用下面这种方式来实现的:
var sliced = arr.slice(index + 1); arr.length = index; arr.push.apply(arr, sliced);
然而,slice方法会返回一个新的数组对象(数组中的元素是原数组中删掉的部分),而且会经过arr.push.apply方法将元素从新复制回原数组,可是在此操做以后,该数组就成为了一片内存垃圾。因为这是咱们引擎中的垃圾产生的热点代码(使用频率很是很高),所以咱们利用了迭代的方式重写了上述代码:
for (var i = index, len = arr.length – 1; i < len; i++) arr[i] = arr[i + 1]; arr.length = len;
显然,重写大量的库函数是很是痛苦的,所以你必须仔细权衡方法的易用性和内存垃圾产生状况。若是产生大量内存垃圾的方法在动画的每一帧中被屡次调用,你可能就会兴高采烈的重写库函数啦。
在递归函数中,经过{}构造空对象,并在递归过程当中传递数据,虽然是很方便的。可是更好的方式是:利用一个单独的数组对象做为堆栈,在递归过程当中对数组进行push和pop操做。更进一步,不要调用array的pop方法(pop将会使得array的最后一个元素将会变成内存垃圾),而应该使用一个索引来记录数组的最后一个元素的位置,在pop时简单的将索引减一便可;相似的,将索引加1来代替array的push操做,只有当索引对应的元素不存在时,才执行真正的push为数组加入一个新元素。
另外,在任什么时候候,都应该避免使用向量对象(例如:包含x和y属性的vector2对象)。有些方法将向量对象做为方法返回值,既能够支持返回值的再次修改,又可以将须要的属性一次性返回,使用起来很是方便。可是有时候在一帧动画中,建立了成百上千个这样的向量对象,从而致使严重的垃圾回收性能问题,也是很是常见的。所以最好将这些方法分离成具备独立职责的功能个体,例如:利用getX()和getY()方法(返回具体数据)代替getPosition()方法(返回一个vector2对象)。
4、总结
在Javascript中,完全避免垃圾回收是很是困难的。垃圾回收机制与实时软件(例如:游戏)的实时性要求,从根本上就是对立的。
可是,为了减小内存垃圾,咱们仍是能够对javascript代码进行完全检查,有些代码中存在明显的产生过多内存垃圾的问题代码,这些正是咱们须要检查而且完善的。
我认为,只要咱们投入更多的精力和关注,实现实时的、低垃圾收集的javascript应用仍是颇有可能的。毕竟,对于可交互性要求较高的游戏或应用来讲,实时性和低垃圾收集,二者都是相当重要。