一、垃圾回收的必要性
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。
二、垃圾回收原理浅析
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
1、标记清除
这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
标记清除法分为标记和清除两个阶段,标记阶段需要从根节点遍历内存中的所有对象,并为可达的对象做上标记,清除阶段则把没有标记的对象(非可达对象)销毁。
标记清除法的优点就是实现简单。
它的缺点有两个,首先是内存碎片化。这是因为清理掉垃圾之后,未被清除的对象内存位置是不变的,而被清除掉的内存穿插在未被清除的对象中,导致了内存碎片化。
第二个缺点是内存分配速度慢。由于空闲内存不是一整块,假设新对象需要的内存是size
,那么需要对空闲内存进行一次单向遍历,找出大于等于size
的内存才能为其分配。
标记清除算法改进—— 标记整理算法
标记清除算法的缺点主要在于内存清理之后剩余的内存位置不变而导致内存碎片化,因此可以使用标记整理算法改进。
标记整理算法的标记阶段与标记清除算法相同,都是从根节点遍历内存中的所有对象,为可达的对象打上一个标记。但是在标记结束后,标记整理算法将这些可达的对象移向内存的一端,然后清理掉边界的内存。
那如何找到合适的块呢?我们可以采取下面三种内存分配策略:
First-fit
,找到大于等于 size 的块立即返回Best-fit
,遍历整个空闲列表,返回大于等于 size 的最小分块Worst-fit
,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回
分类 | First-fit | Best-fit | Worst-fit |
---|---|---|---|
优点 | 分配的速度和效率更高 | -- | 空间利用率看起来是最合理 |
缺点 | -- | -- | 切分之后会造成更多的小块, 形成内存碎片, 所以不推荐使用 |
2、引用计数
另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
在内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要”简化成“对象有没有其他对象引用到它”,如果没有对象引用这个对象,那么这个对象将会被回收。上例子:
let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1
let obj2 = obj1; // A 的引用个数变为 2
obj1 = 0; // A 的引用个数变为 1
obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了
但是引用计数有个最大的问题: 循环引用。
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。
要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
obj1 = null;
obj2 = null;