JavaScript 垃圾回收机制
学习并使用闭包的时候总会在各博客里面看到闭包的坏处有一条:
- 使用不当的闭包将会在 IE(IE9 之前)中造成内存泄漏
IE9 的 JavaScript 引擎使用的垃圾回收算法是 引用计数法,对于循环引用将会导致 GC 无法回收“应该被回收”的内存。造成了无意义的内存占用,也就是内存泄漏。
先科普一下:内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
JS 的垃圾回收机制
在变量进入执行环境时,会添加一个进入标记,当变量离开时,会添加一个离开标记,标记清除是 GC 在运行时会给所有变量加上标记,然后去掉那些还在环境中或还被环境中变量引用的变量,清除剩下还被标记的所有变量。
那么,我有了以下几个疑问:
- 什么是循环引用?
- GC 是什么?它是怎么工作的?
- 为什么引用计数算法将会导致内存无法释放?
- JavaScript(或者说 JavaScript 引擎)还有多少垃圾回收算法?
GC 是什么
GC 是 Garbage Collection 的缩写,意为垃圾回收;这些垃圾是程序不用的内存空间(可能是在之前用过了,以后不会再用的)。那么 GC 就是负责收走垃圾的,因为他工作在 JavaScript 引擎内部,所以对于我们前端开发者来说,GC 在“一定程度上”是悄无声息工作的(注意此处的加引号部分)。
必备的基础概念
- 堆(HEAP)是关于动态存放对象的内存空间, 而对象在 JavaScript 里面是引用类型。
- mutator 这个词意思晦涩,在 GC 里面代表应用程序本身,我们暂且理解为 mutator 需要大量的内存。
- allocator,mutator 将需要内存的申请提交到此,allocator 负责从堆中调取足够内存空间供 mutator 使用。
- 活动对象/非活动对象:代表通过 mutator 引用的对象,举个例子
var a = { name: "bar" }; // '这个对象'被a引用,是活动对象。
a = null; // ‘这个对象’没有被a引用了,这个对象是非活动对象。
常用的几种 GC 算法
引用计数法
顾名思义,让所有对象实现记录下有多少“程序”在引用自己,让各对象都知道自己的“人气指数”。举一个简单的例子:
var a = new Object(); // 此时'这个对象'的引用计数为1(a在引用)
var b = a; // ‘这个对象’的引用计数是2(a,b)
a = null; // reference_count = 1
b = null; // reference_count = 0
// 下一步 GC来回收‘这个对象’了
这个方法有优势也有劣势:
优势
- 可即刻回收垃圾,当被引用数值为 0 时,对象马上会把自己作为空闲空间连到空闲链表上,也就是说。在变成垃圾的时候就立刻被回收。
- 因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的 GC,那么最大暂停时间很短。
- 不用去遍历堆里面的所有活动对象和非活动对象.
劣势
- 计数器需要占很大的位置,因为不能预估被引用的上限,打个比方,可能出现 32 位即 2 的 32 次方个对象同时引用一个对象,那么计数器就需要 32 位。
- 最大的劣势是无法解决循环引用无法回收的问题 这就是前文中 IE9 之前出现的问题
function f() {
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2,o2的引用次数是1
o2.a = o; // o2 引用 o,o的引用此时是1
return "azerty";
}
f();
fn 在执行完成之后理应回收 fn 作用域里面的内存空间,但是因为 o 里面有一个属性引用 o2,导致 o2 的引用次数始终为 1,o2 也是如此,而又非专门当做闭包来使用,所以这里就应该使 o 和 o2 被销毁。 因为算法是将引用次数为 0 的对象销毁,此处都不为 0,导致 GC 不会回收他们,那么这就是内存泄漏问题。
该算法已经逐渐被 ‘标记-清除’ 算法替代,在 V8 引擎里面,使用最多的就是 标记-清除算法
标记清除算法
主要将 GC 的垃圾回收过程分为两个阶段
- 标记阶段:把所有活动对象做上标记。
- 清除阶段:把没有标记(也就是非活动对象)销毁。
标记阶段
根可以理解成我们的全局作用域,GC 从全局作用域的变量,沿作用域逐层往里遍历(对,是深度遍历),当遍历到堆中对象时,说明该对象被引用着,则打上一个标记,继续递归遍历(因为肯定存在堆中对象引用另一个堆中对象),直到遍历到最后一个(最深的一层作用域)节点。
清除阶段
又要遍历,这次是遍历整个堆,回收没有打上标记的对象。
优势:
- 实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示
- 解决了循环引用问题
缺点
- 造成碎片化(有点类似磁盘的碎片化)
- 再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端
复制算法
复制算法配合这张图理解起来非常简单,就是只把某个空间的活动对象复制到其他空间。
将一个内存空间分为两部分,一部分是 From 空间,另一部分是 To 空间,将 From 空间里面的活动对象复制到 To 空间,然后释放掉整个 From 空间,然后此刻将 From 空间和 To 空间的身份互换,那么就完成了一次 GC。