内存会出现的问题:
页面性能一直不好。这可能是因为内存膨胀,就是页面使用的内存比页面最佳速度需要的内存多。
页面执行延迟或经常暂停。 这可能是因为垃圾回收程序进行得很经常。垃圾回收程序进行的时候所有脚本停止进行。
页面性能逐渐变差。这可能是内存泄露,就是程序不再使用的内存垃圾回收程序不回收
[**JavaScript
垃圾回收**](https://segmentfault.com/a/1190000015641168)
JavaScript
内存管理于我们来说是自动的、不可见的。我们创建的原始类型、对象、函数等等,都会占用内存。
当这些数据不被需要后会发生什么?JavaScript
引擎如何发现并清除他们?
可触及(Reachability)
JavaScript
内存管理的关键概念是可触及(**Reachability)**。
简单来说,“可触及”的值就是可访问的,可用的,他们被安全储存在内存。
以下是一些必定“可触及”的值,不管出于任何原因,都不能删除:
当前函数的局部变量和参数。
当前调用链(current chain of nested calls)中所有函数的局部变量和参数。
全局变量。
(以及其他内部变量)
这些值都称为 root。
其他值是否可触及视乎它是否被
root
及其引用链引用。假设有一个对象存在于局部变量,它的值引用了另一个对象,如果这个对象是可触及的,则它引用的对象也是可触及的,后面会有详细例子。
JavaScript
引擎有一个垃圾回收后台进程,监控着所有对象,当对象不可触及时会将其删除。
一个简单例子
//
user 引用了一个对象
let
user = {
name:
‘John’,
};
箭头代表的是对象引用。全局变量 “user” 引用了对象 {name:
“John”}(简称此对象为
John)。John
的 “name” 属性储存的是一个原始值,所以无其他引用。
如果覆盖 user,对
John
的引用就丢失了:
user
= null
现在 John 变得不可触及,垃圾回收机制会将其删除并释放内存。
两个引用
如果我们从 user 复制引用到 admin:
//
user 引用了一个对象
let
user = {
name:
‘John’,
};
let
admin = user;
如果重复一次这个操作:
user
= null
……这个对象是依然可以通过 admin 访问,所以它依然存在于内存。如果我们把 admin 也覆盖为 null,那它就会被删除了。
相互引用的对象
这个例子比较复杂:
function
marry(man,
woman) {
woman.husband
= man;
man.wife
= woman;
return
{
father:
man,
mother:
woman,
}
}
let
family = marry(
{
name:
‘John’,
},
{
name:
‘Ann’,
}
);
marry 函数让两个参数对象互相引用,返回一个包含两者的新对象,结构如下:
暂时所有对象都是可触及的,但我们现在决定移除两个引用:
delete
family.father;
delete
family.mother.husband;
只删除一个引用不会有什么影响,但是两个引用同时删除,我们可以看到
John
已经不被任何对象引用了:
即使
John
还在引用别人,但是他不被别人引用,所以
John
现在已经不可触及,将会被移除。
垃圾回收后:
**孤岛(Unreachable
island)**
也可能有一大堆互相引用的对象整块(像个孤岛)都不可触及了。
对上面的对象进行操作:
family
= null
内存中的情况如下:
这个例子展示了“可触及”这个概念的重要性。
尽管
John
和
Ann
互相依赖,但这仍不足够。
“family” 对象整个已经切断了与
root
的连接,没有任何东西引用到这里,所以这个孤岛遥不可及,只能等待被清除。
内部算法
基础的垃圾回收算法被称为“标记-清除算法”(”mark-and-sweep”):
垃圾回收器获取并标记 root。
然后访问并标记来自他们的所有引用。
访问被标记的对象,标记他们的引用。所有被访问过的对象都会被记录,以后将不会重复访问同一对象。
……直到只剩下未访问的引用。
所有未被标记的对象都会被移除。
举个例子,假设对象结构如下:
很明显右侧有一个“孤岛”,现在使用“标记-清除”的方法处理它。
首先,标记
root:
然后标记他们的引用:
……标记他们引用的引用:
现在没有被访问过的对象会被认为是不可触及,他们将会被删除:
这就是垃圾回收的工作原理。
JavaScript
引擎在不影响执行的情况下做了很多优化,使这个过程的垃圾回收效率更高:
分代收集 — 对象会被分为“新生代”和“老生代”。很多对象完成任务后很快就不再需要了,所以对于他们的清理可以很频繁。而在清理中留下的称为“老生代”一员。
增量收集 — 如果对象很多,很难一次标记完所有对象,这个过程甚至对程序执行产生了明显的延迟。所以引擎会尝试把这个操作分割成多份,每次执行一份。这样做要记录额外的数据,但是可以有效降低延迟对用户体验的影响。
闲时收集 — 垃圾回收器尽量只在 CPU 空闲时运行,减少对程序执行的影响。
除此以外还有很多对垃圾回收的优化,在此就不详细说了,各个引擎有自己的调整和技术,而且这个东西一直随着引擎的更新换代在改变,如果不是有实在的需求,不值得挖太深。不过如果你真的对此有浓厚的兴趣,下面会为你提供一些拓展链接。