最近在重新回顾JavaScript基础,今天是「垃圾回收」!
我们知道,当我们创建原始值、对象、函数等等数据时,都会占用内存,那当我们不再需要某个东西时会发生什么呢,JavaScript引擎是如何发现并清理它的呢?
可达性(Reachability)
JavaScript中一个主要的内存管理概念就是 可达性。"可达"值指的是那些以某种方式可以访问或者可用的值,它们一定是存储在内存之中的。
1.一些不能被释放的值:
当前执行函数的局部变量和参数;
1)当前嵌套调用链上的其他函数的局部变量和参数;
2)所有的全局变量;
以上这些值被称作 根。
2.什么样的值会被视为"可达"呢?
如果全局变量中有一个对象,并且这个对象有一个属性引用了另一个对象,那么"另一个"对象被认为是可达的,包括它引用的内容也是可达的。
一个例子:
js
// user 具有对这个对象的引用
let user = { name: "John" };
这个箭头描述了一个对象的引用,全局变量user引用了对象{ name: "John" },这个John的"name"属性存储一个原始值。现在我们将user进行重写:
js
user = null;
那么现在John就变成不可达的了,因为它失去了引用不能被访问了,垃圾回收器就会认为它是垃圾数据并进行垃圾回收,释放内存。这也就是 垃圾回收机制。
两个引用
我们现在将user引用复制给admin,再将user赋值为null,又会发生什么呢?
js
let user = { name: "John" };
let admin = user;
user = null;
事实上就是{ name: "John" }是仍然可以通过admin这个全局变量被访问到的,所以这个对象必须被保留在内存中。如果我们将admin也赋值null,那么{ name: "John" }对象也会被删除。
相互关联的两个对象
下面的例子展示了一个家庭,通过marry函数使两个对象之间相互引用产生了关联关系:
js
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({ name: "Jhon"}, {name: "Ann"});
他们的内存结构应该是这样的,所有对象都是可达的:
现在我们移除两个引用:
js
delete family.father;
detete family.mother.husband;
这时我们发现 {name: "Jhon"} 对象依旧可以通过 man.wife = woman 实现可达。但是如果我们将man.wife = woman 也置空,那么我们就会发现没有对Jhon的引用了。
所以现在Jhon是不可达的,就会从内存中被删除,同时Jhon的所有数据也会变得不可达。经过垃圾回收以后,现在对象变成了这样:
无法到达的岛屿
如果我们直接将family赋值为null,虽然Jhon和Ann依旧有着关联关系,但他们已经不与根相连,那么他们就变成了一座"孤岛",他们会被从内存中删除。
js
family = null;
内部算法
垃圾回收的基本算法被称为"mark-and-sweep"。
定期执行以下"垃圾回收"的步骤:
1)垃圾收集器找到所有的根,并"标记"它们;
2)然后它遍历并"标记"所有来自它们的引用;
3)然后它遍历标记的对象并标记 它们的 引用。这样所有被遍历到的对象都会被记住;
4)一直遍历下去,直到所有可达的引用都被访问到;
5)没有被标记的对象就会被删除。
比如当前的数据结构是这样的:
我们可以看到右侧现在有一个"孤岛",那么垃圾回收会怎样处理它呢?
第一步:标记所有的根
第二步:标记遍历出来的它们的所有引用的对象
如果还有引用的话,就继续标记
当所有的对象都标记完成后,就会发现右侧"孤岛"不可达,就会将它删除。
这就是垃圾回收,JavaScript引擎做了许多优化,可以使垃圾回收运行速度更快,也不会对代码执行引入任何延迟。这里暂时不说啦!
总结
- 垃圾回收是自动完成的,我们并不能强制执行或阻止执行
- 当对象是可达状态时,它一定是存于内存之中的
- 被引用和可访问是不同的,被引用的对象可能存在整体都不可达,比如上面的family赋值为null的例子。