学习链接
垃圾回收
垃圾回收的概念
JavaScript 代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。
垃圾回收的方式
标记清除(mark-and-sweep)
说法一
定期执行以下”垃圾回收步骤”:
- 垃圾回收器找到所有的根,并标记(记住)它们。(见补充)
- 然后遍历并标记来自它们(根)的所有引用。
- 然后遍历并标记来自上一步的被标记对象的所有引用。
所有被遍历的对象都会被记住,以免再次遍历到同一个对象。 - 重复前面的操作,直到所有可达的引用(从根部)都被访问到并标记。
- 将未被标记的对象删除。
说法二
- 垃圾回收程序运行的时候,会标记内存中存储的所有变量。
- 然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。
- 在此之后再被加上标记的变量就是待删除的了,因为任何在上下文中的变量都访问不到它们。
- 随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
优化建议
- 分代收集(Generational collection)
对象被分成两组:“新的”和“旧的”。许多对象出现,完成它们的工作并很快死去,它们可以很快被清理。那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少。 - 增量收集(Incremental collection)
如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做。然后将这几部分会逐一进行处理。这需要它们之间有额外的标记来追踪变化,但是这样会有许多微小的延迟而不是一个大的延迟。 - 闲时收集(Idle-time collection)
垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
引用计数(reference counting)
主要思路是跟踪记录每个值被引用的次数。
- 声明变量并给它赋一个引用值时,这个值的引用数为 1。
如果同一个值又被赋给另一个变量,那么引用数加 1。 - 如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。
- 当一个值的引用数为 0 时,就说明没办法再访问到这个值了。
- 垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
这种方法会引起循环引用的问题,即对象 A 和对象 B 通过各自的属性相互引用,此时,两个对象的引用数都是 2。
function problem() {
let objectA = new Object();
let objectB = new Object();
objectA.prop = objectB;
objectB.prop = objectA;
}
这会导致这两个对象在函数结束后依旧存在,因为它们的引用数永远不会变为 0。如果函数被多次调用,则会导致大量内存永远不会被释放。
解除引用
objectA.prop = null;
objectB.prop = null;
优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。
如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用。
优化
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化:在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为 0,以此来达到清空数组的目的。
- 对
**object**
进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 - 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
内存泄漏
- 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
- 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
小结
- 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
- 当对象是可达状态时,它一定是存在于内存中的。
- 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。
- 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
- 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次,次数为 0 的会被回收内存。
- 引用计数在代码中存在循环引用时会出现问题。
- 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。
补充
可达性
简而言之,”可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。
- 这里列出固有的可达值的基本集合,这些值明显不能被释放。
比方说:- 当前执行的函数,它的局部变量和参数。
- 当前嵌套调用链上的其他函数、它们的局部变量和参数。
- 全局变量。
- (还有一些其他的内部的值)
这些值被称作 根(roots)。
- 如果任何其他值可以通过引用或引用链从根访问,则认为它是可达的。
例如,如果全局变量中有一个对象,并且该对象具有引用另一个对象的属性,则该对象被认为是可访问的。 它所引用的那些也是可以访问的。
在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。
相互关联的对象
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
目前为止,所有对象都是可达的。
delete family.father;
delete family.mother.husband;
对外引用不重要,只有传入引用才可以使对象可达,
也就是说,无法通过引用或引用链从根访问到**"John"**
所在的对象。
所以,"John"
所在的对象现在是不可达的,并且将被从内存中删除,同时 "John"
的所有数据也将变得不可达。
无法到达的岛屿
如果任何其他值可以通过引用或引用链从根访问,则认为它是可达的。
下面的几个对象相互引用,但外部没有对其任意对象的引用,
也就是说,无法通过引用或引用链从根访问到里面的任意一个对象,
所以,这些对象也可能是不可达的,会被从内存中删除。
family = null;