零散专题34 JavaScript的垃圾回收机制
手动回收?
JavaScript在创建变量时自动进行了内存分配,并且在不使用时自动释放。释放的过程叫做垃圾回收。
JavaScript没有暴露任何垃圾回收期的接口,其内存管理是自动执行且不可见的,所以开发者没有办法手动进行强制的垃圾回收,也没有办法干预内存管理。
在多数情况下,开发者也没有必要手动解除对象的引用,只要简单地把变量放在他们应该的地方(局部变量),垃圾就能正确被回收
V8的内存分配
当声明变量并赋值时,V8就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量,V8就会继续申请,直到堆的大小到了V8的内存上限为止。
堆内存分为两种分配方式:
- 静态分配,全局变量、函数之类的分配方式,它们在页面没有关闭之前,是不会被清除的
- 动态分配,使用
new
创建出来的,主动要求给分配空间的
可达性
JavaScript中内存管理关键的一个概念是可达性,一个变量的“可达性”值得就是它能够被以某种方式访问到或者引用到,被保证存储到内存中。JavaScript引擎中有一个名为“垃圾回收器”的进程,它监视所有对象,并删除不可访问的对象。
一些常见的具有可达性,会常驻内存的情况:
(1)全局变量,全局变量会常驻内存,除非刷新页面、离开页面时才会被清理
(2)对象引用
function ob(){
var bar = new largeObject(); // 很大一个对象\变量\字符串
bar.someCall();
return bar;
}
var a = new ob();
现在有一个指向bar
对象的引用,当ob
调用结束后,bar
对象不会被回收,直到变量a
分配其他引用(或者a
超出了作用域范围)。
(3)DOM事件,即使DOM元素被移除,其绑定的事件不会被回收,除非使用removeEventListener
显式的移除事件
(4)定时器,除非显式的清楚定时器
例子
假设定义了这样一个变量:
let user = {
name: 'John'
}
user
这个变量引用了一个对象{name: 'John'}
:
如果将user
的值覆盖user = null
,则引用丢失,{name: 'John'}
会被垃圾回收器清除,分配的内存被回收。
如果不将user
的值覆盖,而是把user
复制给另一个变量admin
:
let admin = user
这是两个变量都引用了同一个对象{name: 'John'}
:
这个时候再将user
的值覆盖user = null
,虽然user
不再引用{name: 'John'}
,但是admin
仍然与{name: 'John'}
保持引用关系,所以{name: 'John'}
不会被垃圾回收器清除,分配的内存也不会被回收。
复杂些的例子
函数marry
通过给两个对象彼此提供引用来“联姻”它们,并返回一个包含两个对象的新对象
function marry (man, woman) {
woman.husban = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
})
产生的内存结构:
image现在如果想要清除{name: John}
这个变量,必须要同时删除外界对它的所有引用:
delete family.father;
delete family.mother.husband;
image
它对外的引用man.wife
是否删除不重要,因为只有外界对变量的引用,才决定了这个变量是否是“可达”的。现在虽然{name: John}
仍然通过wife
属性引用了{name: 'Ann'}
,但是外界对它的引用都删除了,所以它是不可达的,所以会被垃圾回收器清除。
也有可能整个对象变的不可访问从而被从内存中删除,例如我们切断family
的引用family = null
,那么:
虽然{name: John}
和{name: 'Ann'}
仍然相互引用,但是family
已经从根上断开了连接,不再有对它们的外界的引用(也就是说无法从局部或者全局变量访问到它们),所以下面整个块都是不可达的,整体都会被删除。
垃圾回收的算法
标记-清除算法
垃圾回收的基本算法是“标记-清除”,具体步骤如下,会被定期执行:
(1)垃圾回收器获取「根」并标记
image(2)继续访问并标记所有来自「根」的引用,所有标记过引用都不会被重复访问:
image(3)继续标记引用的后代引用,直到所有的“可达”(可以从根访问的)的引用全部被标记为止
image(4)删除所有未标记的对象,回收它们占用的内存:
image这就是JavaScript垃圾回收期的工作原理,JavaScript引擎应用了许多优化,使其运行的更快,并且不影响执行,例如:
(1)分代回收
对象分为两组“老对象”和“新对象”,许多新对象出现,完成任务后迅速被解除应用,它们很快被清理。而那些存活时间足够久的对象就会认为是“老对象”,很少接受检查
(2)增量回收
如果试图一次性遍历并标记整个对象集,那么花费时间会很长,因此引擎会试图将垃圾回收分解为多个部分,然后各个部分分别执行。这需要额外的标记来跟踪变化
(3)空闲时间收集
垃圾回收期只在CPU空闲时运行,以减少对正常任务的影响
除了“标记-清除”算法之外,还有其他的多种算法,比如“引用计数”算法,记录每个对象被引用的次数,每次新建对象、赋值引用和删除引用的同时更新计数器,如果计数器值为0则直接清除回收内存。这篇文章介绍了多种算法,并分析了优缺点,写的很好。
引用计数算法
引用计数算法是最初级的垃圾收集算法,此算法把“对象是否不再需要”简化定义为“有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收器回收。
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”的原始引用o被o2替换了
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 最初的对象现在已经是零引用了
// 他可以被垃圾回收了
// 然而它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
这种算法没有办法处理循环引用的情况,例如下面的例子,两个对象被创建,并相互引用,形成循环。它们被调用后不再被外部引用,没用了,可以被回收了。但是引用计数算法认为它们都还至少有一次引用,所以不会被回收,一直保存在内存中。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
经验法则
为了使Chrome的垃圾回收器不保留不再需要的对象,有几点需要牢记:
- 在恰当的作用域中使用变量,尽量在函数作用域中声明变量,尽量声明局部变量,尽量避免全局变量
- 确保移除不再需要的事件监听器,比如即将被移除的DOM对象所绑定的事件
- 避免缓存大量不会被重用的数据
- 少用闭包
- 不要再生产环境使用
console.log()
、console.error()
、console.dir()
等方法打印任何复杂对象,因为这些对象不会被垃圾回收器回收。