垃圾回收机制
JS具有垃圾回收机制, 执行环境会负责管理代码执行过程中使用的内存。
基本原理: 隔一段时间或者按照固定的时间间隔,找出那些不再继续使用的变量,然后释放其占用的内存。
分析函数局部变量
局部变量只在执行函数的过程中存在,这个过程中会为局部变量在栈内存上分配相应的空间,以便存储它们的值。 然后在函数中使用这些变量,直至函数执行结束。
结束后这些局部变量就没用了,因此可以释放它们的内存以供将来使用。
问题
不是所有情况都这么容易就能得出结论
垃圾收集器必须跟踪哪个变量有用,哪个变量没用。 对于不再有用的变量打上标记,以备将来收回其占用的内存。 跟踪! 标记!
变量生命周期
全局变量的生命周期,直到浏览器关闭页面才结束。
局部变量只在函数的执行过程中存在,这个过程会在栈或堆上分配相应ed空间,存储它们的值。
(闭包的话,由于内部函数的原因,外部函数的变量对象仍然被保存着)
一旦函数结束了,局部变量没有存在的必要了,可以释放内存。 貌似函数结束了,但是其实对于垃圾回收来说,它必须知道哪个变量有用,哪个没用,对于不再有用的变量打上标记!
标记清除
变量进入环境时,就将这个变量标记为”进入环境”。。
当变量离开环境时,将其标记为“离开环境“~ 所以这里有两个状态。
逻辑上讲,永远不能释放进入环境的变量所占用的内存~
Marked可以用任何方式来标记变量,比如特殊位的反转,维护一个列表。
1. 垃圾收集器在运行的时候会给存储在内存中的所有变量加上标记。
2. 然后,它会去掉环境中的变量以及被环境中的变量所引用的变量(闭包) 的标记。 去除标记
3. 在这些完成之后仍然存在的标记的变量,视为要删除的变量。
原因是环境中的变量已经无法访问到这些变量了。 最后垃圾收集器完成内存清除的工作,销毁那些带标记的值并回收它们占用的内存。
例如:我们设置一个“进入环境”的变量列表, 一个“离开环境”的变量列表
跟踪哪些变量发生了变化。 如何标记变量其实不重要,重要在于采取什么策略。
引用计数
另外一种不太常见的垃圾收收集器策略叫做引用计数reference counting!
引用计数就是跟踪记录每个值被引用的次数。
比如你声明了一个变量,并且将一个引用类型值赋给这个变量, 则这个值的引用次数就是1~
引用次数+1 如果同一个值又被赋给另一个变量,引用计数又加1!!
引用次数-1 如果包含这个值引用的变量又取得了另一个值,则这个值引用次数减1!
当这个值为0(说明没有办法再访问这个值了,没有存储它的变量了!), 就可以将其占用的内存空间回收回来!
循环引用
function problem(){
var objA=new Object();
var objB=new Object();
objA.anotherObject=objB;
objB.someObnj=objA
}
循环引用的原因就像这个代码所示。
标记清除中,由于函数执行后,两个对象离开了作用域,因此这种相互引用不是个问题。
但在采用引用计数策略的实现中,当函数执行完毕后,objA objB 还将继续存在, 因为它们的引用次数不会是0 。
如果这个函数被重复多次调用,就会导致大量内存得不到回收
IE中有一部分对象不是原生JS对象,而是C++以COM组件对象模型的形式实现的。
COM对象的垃圾回收机制采用的是引用计数策略。
避免方法
myObject.element=null;
element.someObject=null;
将变量设置为null,意味着切断变量和它此前引用的值之间的连接。 当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
性能问题
垃圾收集器是周期性运行的, 如果为变量分配的内存数量很大,那么回收的工作量也会很大。 确定垃圾回收的间隔就很重要。
IE的策略:
根据内存分配量, 比如256个变量,4096个对象,或64KB字符串。 达到任何一个临界值就会运行垃圾收集器。
问题: 如果一个脚本包含了的变量就是临界值,那么这个脚本可能会一直都拥有这么多的变量。垃圾回收器就不得不频繁运行。
IE7 动态修正,触发垃圾回收的临界值。
管理内存
使用具备垃圾收集机制的语言编写程序。
有一点不同就是 浏览器的可用内存是有限的,出于安全考虑防止JS的网页耗尽全部系统内存而导致系统崩溃
所以我们一定要解除引用dereference 。
为执行的代码只保存必要的数据。一旦数据不再可用就将其值设置为null 释放其引用。
function createPerson(name){
var localPerson=new Object();
localPerson.name=name;
return localPerson;
}
var globalPerson=createPerson('xx');
//解除引用
globalPerson=null;
建议 :对全局变量和全局对象的属性,解除引用。 因为它们不会被自动解除引用,要手工解除!
局部变量会在它们离开执行环境时自动被dereference
解除引用的真正作用是让值脱离执行环境,以便垃圾回收器下次回收!
V8引擎的垃圾回收机制
此外我们提一下V8的垃圾回收机制。
V8是有内存限制的,它限制堆的大小的。
分代式垃圾回收机制
V8主要是分代式垃圾回收机制。实际上垃圾回收算法跟场景关系很大,现代的垃圾回收算法中按对象的存活时间将垃圾回收进行不同得分代,然后对不同的分代的内存施以更高效的算法。
老生代 和 新生代
老生代:存活时间较长或常驻内存的对象
新生代:存活时间较短的对象。
V8堆的大小就是:新生代和老生代的内存空间相加
Scavenge算法
新生代使用Scavenge算法来进行垃圾回收。 在Scavenge的具体实现中,主要采用了Cheney算法。采用复制的方式实现。
主要思想:
将堆内存一分为二,每一部分空间称为semispace。这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。 处于使用状态的是from空间,空闲的是to空间。分配对象的时候先从From空间分配。 垃圾回收的时候检查From空间中的存活对戏,这些存活对象被复制到To空间中,非存活对象被释放。
垃圾回收过程:就是通过将存活对象在两个semispace空间之间复制
缺点:只用了堆内存的一半!
只复制存对象,适用于生命周期短的! 牺牲空间换时间!
Mark-Sweep& Mark-Compact
标记清除算法。 前面讲到了。
因为它遍历堆中所有的对象,只清除没有被标记的对象。
Mark-Sweep只处理死亡的对象。
一次标记清除回收后,内存会出现不连续的情况。
Mark-Compact
Mark-Compact是标记整理,为了解决标记清除过程中出现的碎片。
对象死亡后,将活的对象往一端移动,移动完成后,直接清理掉边界外的内存。
三种算法对比
Incremental Marking
垃圾回收3种基本算法都要停止应用逻辑,来执行垃圾回收,回收完之后才进行应用逻辑。
V8中,对于新生代的算法影响不大,停顿很小。但是对于老生代的,全堆垃圾回收,需要标记,清理,整理等,比较可怕。
我们使用增量标记,将之前一次停顿完成的动作拆成许多小的步骤。
增量标记改进了1/6的性能