一、垃圾回收(garbage /ˈɡɑːrbɪdʒ/ collector)是自动完成的,我们不能强制执行或是阻止执行。
二、当对象是可达状态时,它一定是存在于内存中的。
三、被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。

可达性(Reachability)

一、JavaScript 中主要的内存管理概念是可达性。
二、简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。
1、这里列出固有的可达值的基本集合,这些值明显不能被释放。比方说:这些值被称作根(roots)。

  • 当前函数的局部变量和参数。
  • 嵌套调用时,当前调用链上所有函数的变量与参数。
  • 全局变量。
  • (还有一些内部的)

2、如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则该对象被认为是可达的。而且它引用的内容也是可达的。
三、在 JavaScript 引擎中有一个被称作垃圾回收器的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。

示例

| ☆【示例】这里是一个最简单的例子:```javascript // user 具有对这个对象的引用 let user = { name: “John” };

  1. 二、示意图:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/355497/1619333780101-05349a11-9e29-4737-ac43-417449132e53.png#clientId=u92c3773b-7c53-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=164&id=rqtxB&margin=%5Bobject%20Object%5D&name=image.png&originHeight=164&originWidth=151&originalType=binary&ratio=1&rotation=0&showTitle=false&size=4579&status=done&style=none&taskId=ufdaf4f2d-72d1-4d55-b6b1-7e86068c9c8&title=&width=151)<br /><br />1、这里的箭头描述了一个对象引用。全局变量"user"引用了对象{name:"John"}(为简洁起见,我们称它为 John)。John 的"name"属性存储一个原始值,所以它被写在对象内部。<br />三、如果user的值被重写了,这个引用就没了:```javascript
  2. user = null;

四、示意图
image.png

1、现在 John 变成不可达的了。因为没有引用了,就不能访问到它了。垃圾回收器会认为它是垃圾数据并进行回收,然后释放内存。 | | —- |

两个引用

| ☆【示例】现在让我们想象下,我们把user的引用复制给admin:```javascript // user 具有对这个对象的引用 let user = { name: “John” };

let admin = user;

二、示意图<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/355497/1619333883995-9a52e712-7045-428f-9b1d-22bd73052c3b.png#clientId=u92c3773b-7c53-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=150&id=nnOis&margin=%5Bobject%20Object%5D&name=image.png&originHeight=150&originWidth=160&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5284&status=done&style=none&taskId=u17034c2b-5e12-457d-9cdb-702eb28b6d3&title=&width=160)<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 />![image.png](https://cdn.nlark.com/yuque/0/2021/png/355497/1619333951994-9d5a4bd1-0ecb-4feb-8214-4616cf3cca2a.png#clientId=u92c3773b-7c53-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=220&id=fmfcs&margin=%5Bobject%20Object%5D&name=image.png&originHeight=220&originWidth=356&originalType=binary&ratio=1&rotation=0&showTitle=false&size=13197&status=done&style=none&taskId=u87ee189c-563c-4349-a228-350c9775e7c&title=&width=356)<br />三、到目前为止,所有对象都是可达的。<br />四、现在让我们移除两个引用:```javascript
delete family.father;
delete family.mother.husband;

image.png
五、仅删除这两个引用中的一个是不够的,因为所有的对象仍然都是可达的。
六、但是,如果我们把这两个都删除,那么我们可以看到再也没有对 John 的引用了:
image.png
七、对外引用不重要,只有传入引用才可以使对象可达。所以,John 现在是不可达的,并且将被从内存中删除,同时 John 的所有数据也将变得不可达。
八、经过垃圾回收:
image.png | | —- |

无法到达的岛屿

一、几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。

| ☆-2【示例】源对象与上面相同。然后:```javascript family = null;

三、内存内部状态将变成:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/355497/1619334147328-ad316f44-4779-42f8-a8b9-cf42ca83de33.png#clientId=u92c3773b-7c53-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=284&id=L6UsY&margin=%5Bobject%20Object%5D&name=image.png&originHeight=284&originWidth=442&originalType=binary&ratio=1&rotation=0&showTitle=false&size=15575&status=done&style=none&taskId=u83a879d7-1b4b-48fb-9bd5-d92e5f91898&title=&width=442)<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”。

  1. 这个算法把“对象不再需要”简化定义为“对象是否可以获得”.

二、标记-清除算法家假定设置一个叫做根(root)的对象(在JavaScript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象…从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

  1. 定期执行以下“垃圾回收”步骤:
  • 垃圾收集器找到所有的根,并“标记”(记住)它们。
  • 然后它遍历并“标记”来自它们的所有引用。
  • 然后它遍历标记的对象并标记它们的引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
  • ……如此操作,直到所有可达的(从根部)引用都被访问到。
  • 没有被标记的对象都会被删除。 | 【示例】使我们的对象有如下的结构,我们可以清楚地看到右侧有一个“无法到达的岛屿”。现在我们来看看“标记和清除”垃圾收集器如何处理它。
    image.png
    1、第一步标记所有的根:
    image.png
    2、然后它们的引用被标记了:
    image.png

    3、如果还有引用的话,继续标记:
    image.png
    4、现在,无法通过这个过程访问到的对象被认为是不可达的,并且会被删除。
    image.png
    5、我们还可以将这个过程想象成从根溢出一个巨大的油漆桶,它流经所有引用并标记所有可到达的对象。然后移除未标记的。
    6、这是垃圾收集工作的概念。JavaScript 引擎做了许多优化,使垃圾回收运行速度更快,并且不影响正常代码运行。 | | —- |

三、优点:
“标记-清除算法”比“引用计数垃圾收集”要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
四、20220126-aSuncat:从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。素有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法和它对“对象是否不再需要”的简化定义。
五、循环引用不再是问题。

☆-1【示例】在上面的示例(☆-1)中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。
☆-2【示例】在上面的示例(☆-2)中,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。

限制

一、限制: 那些无法从根对象查询到的对象都将被清除。

  1. 尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。

    优化

    一、一些优化建议:
  • 分代收集(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”,因为网上关于它的文章最丰富的。对于其他引擎,许多方法是相似的,但在垃圾收集上许多方面有所不同。
四、当你需要底层的优化时,对引擎有深入了解将很有帮助。