一、垃圾回收(garbage /ˈɡɑːrbɪdʒ/ collector)是自动完成的,我们不能强制执行或是阻止执行。
二、当对象是可达状态时,它一定是存在于内存中的。
三、被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。
可达性(Reachability)
一、JavaScript 中主要的内存管理概念是可达性。
二、简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。
1、这里列出固有的可达值的基本集合,这些值明显不能被释放。比方说:这些值被称作根(roots)。
- 当前函数的局部变量和参数。
- 嵌套调用时,当前调用链上所有函数的变量与参数。
- 全局变量。
- (还有一些内部的)
2、如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则该对象被认为是可达的。而且它引用的内容也是可达的。
三、在 JavaScript 引擎中有一个被称作垃圾回收器的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。
示例
| ☆【示例】这里是一个最简单的例子:```javascript // user 具有对这个对象的引用 let user = { name: “John” };
二、示意图:<br /><br /><br />1、这里的箭头描述了一个对象引用。全局变量"user"引用了对象{name:"John"}(为简洁起见,我们称它为 John)。John 的"name"属性存储一个原始值,所以它被写在对象内部。<br />三、如果user的值被重写了,这个引用就没了:```javascript
user = null;
四、示意图
1、现在 John 变成不可达的了。因为没有引用了,就不能访问到它了。垃圾回收器会认为它是垃圾数据并进行回收,然后释放内存。 |
| —- |
两个引用
| ☆【示例】现在让我们想象下,我们把user的引用复制给admin:```javascript // user 具有对这个对象的引用 let user = { name: “John” };
let admin = user;
二、示意图<br /><br />三、现在如果执行刚刚的那个操作:```javascript
user = null;
1、然后对象仍然可以被通过admin这个全局变量访问到,所以对象还在内存中。如果我们又重写了admin,对象就会被删除。 |
| —- |
相互关联的对象
| ☆-2【示例】现在来看一个更复杂的例子。这是个家庭:```javascript function marry(man, woman) { woman.husband = man; man.wife = woman;
return { father: man, mother: woman } }
let family = marry({ name: “John” }, { name: “Ann” });
二、marry函数通过让两个对象相互引用使它们“结婚”了,并返回了一个包含这两个对象的新对象。<br />三、由此产生的内存结构:<br /><br />三、到目前为止,所有对象都是可达的。<br />四、现在让我们移除两个引用:```javascript
delete family.father;
delete family.mother.husband;
五、仅删除这两个引用中的一个是不够的,因为所有的对象仍然都是可达的。
六、但是,如果我们把这两个都删除,那么我们可以看到再也没有对 John 的引用了:
七、对外引用不重要,只有传入引用才可以使对象可达。所以,John 现在是不可达的,并且将被从内存中删除,同时 John 的所有数据也将变得不可达。
八、经过垃圾回收: |
| —- |
无法到达的岛屿
一、几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。
| ☆-2【示例】源对象与上面相同。然后:```javascript family = null;
三、内存内部状态将变成:<br /><br />四、这个例子展示了可达性概念的重要性。<br />五、显而易见,John 和 Ann 仍然连着,都有传入的引用。但是,这样还不够。<br />六、前面说的"family"对象已经不再与根相连,没有了外部对其的引用,所以它变成了一座“孤岛”,并且将被从内存中删除 |
| --- |
<a name="ZQt35"></a>
# 内部算法 / 垃圾回收算法
一、对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用,常用垃圾回收算法有下面两种。
- 引用计数(现代浏览器不再使用)
- 标记-清除(常用)
<a name="EM1fq"></a>
## 引用计数垃圾收集
一、引用计数垃圾收集是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
<a name="hVRyE"></a>
### 引用
一、垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。
| 【示例】例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。<br />在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。 |
| --- |
| ☆-1【示例】示例```javascript
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
| | —- |
限制:循环引用
一、该算法有个限制:无法处理循环引用的事例。
| 【示例】在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。```javascript function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o
return “azerty”; }
f();
|
| --- |
| ☆-2【示例】实际例子如下:<br />IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:```javascript
var div;
window.onload = function(){
div = document.getElementById("myDivElement");
div.circularReference = div;
div.lotsOfData = new Array(10000).join("*");
};
在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放。 | | —- |
标记-清除算法
一、20220125-aSuncat:垃圾回收的基本算法被称为标记-清除算法/ “mark-and-sweep”。
- 这个算法把“对象不再需要”简化定义为“对象是否可以获得”.
二、标记-清除算法家假定设置一个叫做根(root)的对象(在JavaScript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象…从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
- 定期执行以下“垃圾回收”步骤:
- 垃圾收集器找到所有的根,并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记它们的引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
- ……如此操作,直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
| 【示例】使我们的对象有如下的结构,我们可以清楚地看到右侧有一个“无法到达的岛屿”。现在我们来看看“标记和清除”垃圾收集器如何处理它。
1、第一步标记所有的根:
2、然后它们的引用被标记了:
3、如果还有引用的话,继续标记:
4、现在,无法通过这个过程访问到的对象被认为是不可达的,并且会被删除。
5、我们还可以将这个过程想象成从根溢出一个巨大的油漆桶,它流经所有引用并标记所有可到达的对象。然后移除未标记的。
6、这是垃圾收集工作的概念。JavaScript 引擎做了许多优化,使垃圾回收运行速度更快,并且不影响正常代码运行。 | | —- |
三、优点:
“标记-清除算法”比“引用计数垃圾收集”要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
四、20220126-aSuncat:从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。素有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法和它对“对象是否不再需要”的简化定义。
五、循环引用不再是问题。
☆-1【示例】在上面的示例(☆-1)中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。 |
---|
☆-2【示例】在上面的示例(☆-2)中,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。 |
---|
限制
一、限制: 那些无法从根对象查询到的对象都将被清除。
- 分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。许多对象出现,完成它们的工作并很快死去,它们可以很快被清理。那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少。
- 增量收集(Incremental collection)—— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做。然后将这几部分会逐一进行处理。这需要它们之间有额外的标记来追踪变化,但是这样会有许多微小的延迟而不是一个大的延迟。
- 闲时收集(Idle-time collection)—— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
二、还有其他垃圾回收算法的优化和风格。不同的引擎会有不同的调整和技巧。随着引擎的发展,情况会发生变化,所以在没有真实需求的时候,“提前”学习这些内容是不值得的。
三、现代引擎实现了垃圾回收的高级算法。
1、《The Garbage Collection Handbook: The Art of Automatic Memory Management》(R. Jones 等人著)这本书涵盖了其中一些内容。
2、如果你熟悉底层(low-level)编程,关于 V8 引擎垃圾回收器的更详细信息请参阅文章V8 之旅:垃圾回收。
(1)V8 博客还不时发布关于内存管理变化的文章。
(2)为了学习垃圾收集,你最好通过学习 V8 引擎内部知识来进行准备,并阅读一个名为Vyacheslav Egorov的 V8 引擎工程师的博客。我之所以说 “V8”,因为网上关于它的文章最丰富的。对于其他引擎,许多方法是相似的,但在垃圾收集上许多方面有所不同。
四、当你需要底层的优化时,对引擎有深入了解将很有帮助。