学习链接

现代JavaScript教程:垃圾回收

JavaScript高级程序设计(第4版)

JavaScript 内存泄漏教程

垃圾回收

垃圾回收的概念

JavaScript 代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收

这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

垃圾回收的方式

标记清除(mark-and-sweep)

说法一

定期执行以下”垃圾回收步骤”:

  • 垃圾回收器找到所有的根,并标记记住)它们。(见补充
  • 然后遍历标记来自它们()的所有引用
  • 然后遍历标记来自上一步的被标记对象所有引用
    所有被遍历的对象都会被记住以免再次遍历到同一个对象
  • 重复前面的操作,直到所有可达的引用(从根部)都被访问到并标记。
  • 未被标记的对象删除

说法二

  • 垃圾回收程序运行的时候,会标记内存中存储的所有变量
  • 然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉
  • 在此之后再被加上标记的变量就是待删除的了,因为任何在上下文中的变量都访问不到它们。
  • 随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

优化建议

  • 分代收集(Generational collection)
    对象被分成两组:“新的”和“旧的”。许多对象出现,完成它们的工作并很快死去,它们可以很快被清理。那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少
  • 增量收集(Incremental collection)
    如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做。然后将这几部分会逐一进行处理。这需要它们之间有额外的标记来追踪变化,但是这样会有许多微小的延迟而不是一个大的延迟。
  • 闲时收集(Idle-time collection)
    垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

引用计数(reference counting)

主要思路是跟踪记录每个值被引用的次数

  • 声明变量并给它赋一个引用值时,这个值的引用数为 1。
    如果同一个值又被赋给另一个变量,那么引用数加 1。
  • 如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。
  • 当一个值的引用数为 0 时,就说明没办法再访问到这个值了
  • 垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存

这种方法会引起循环引用的问题,即对象 A 和对象 B 通过各自的属性相互引用,此时,两个对象的引用数都是 2。

  1. function problem() {
  2. let objectA = new Object();
  3. let objectB = new Object();
  4. objectA.prop = objectB;
  5. objectB.prop = objectA;
  6. }

这会导致这两个对象在函数结束后依旧存在,因为它们的引用数永远不会变为 0。如果函数被多次调用,则会导致大量内存永远不会被释放。

解除引用

  1. objectA.prop = null;
  2. objectB.prop = null;

优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。

如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用

优化

虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。

  • 对数组进行优化:在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为 0,以此来达到清空数组的目的。
  • **object** 进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
  • 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数的外面。

内存泄漏

  1. 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  2. 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  3. 获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
  4. 不合理的使用闭包,从而导致某些变量一直被留在内存当中。

小结

  • 垃圾回收是自动完成的,我们不能强制执行或是阻止执行
  • 当对象是可达状态时,它一定是存在于内存中的。
  • 被引用可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次,次数为 0 的会被回收内存。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。

补充

可达性

简而言之,”可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

  1. 这里列出固有的可达值的基本集合,这些值明显不能被释放。
    比方说:
    • 当前执行的函数,它的局部变量和参数。
    • 当前嵌套调用链上的其他函数、它们的局部变量和参数。
    • 全局变量。
    • (还有一些其他的内部的值)

这些值被称作 根(roots)

  1. 如果任何其他值可以通过引用或引用链从根访问,则认为它是可达的
    例如,如果全局变量中有一个对象,并且该对象具有引用另一个对象的属性,则该对象被认为是可访问的。 它所引用的那些也是可以访问的。

在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的

相互关联的对象

  1. function marry(man, woman) {
  2. woman.husband = man;
  3. man.wife = woman;
  4. return {
  5. father: man,
  6. mother: woman
  7. }
  8. }
  9. let family = marry({
  10. name: "John"
  11. }, {
  12. name: "Ann"
  13. });

垃圾回收 - 图1

目前为止,所有对象都是可达的。

  1. delete family.father;
  2. delete family.mother.husband;

垃圾回收 - 图2

对外引用不重要,只有传入引用才可以使对象可达

也就是说,无法通过引用或引用链从根访问到**"John"** 所在的对象

所以,"John" 所在的对象现在是不可达的,并且将被从内存中删除,同时 "John" 的所有数据也将变得不可达。

垃圾回收 - 图3

无法到达的岛屿

如果任何其他值可以通过引用或引用链从根访问,则认为它是可达的。

下面的几个对象相互引用,但外部没有对其任意对象的引用

也就是说,无法通过引用或引用链从根访问到里面的任意一个对象

所以,这些对象也可能是不可达的,会被从内存中删除。

  1. family = null;

垃圾回收 - 图4