浏览器垃圾回收机制
浏览器的垃圾回收机制(Garbage collection
), 简称 GC, 它会周期性运行以释放那些不需要的内存, 否则, JavaScript 的解释器将会耗尽全部系统内存而导致系统崩溃。具体到浏览器中的实现, 通常有两个策略: 标记清除和引用计数。
引用计数法
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用), 对象将被垃圾回收机制回收。引用计数法是最初级的垃圾收集算法, 如果某对象没有其他对象指向它了, 那就说明它可以被回收。但是它无法处理循环引用的问题。我们执行 f 函数, 它返回了一个数字, 和内部的 o1,o2 没什么关系, 但是对引用计数法来说, o1,o2 它们之间还存在着相互引用, 并不会被回收。这就造成了内存泄漏。
标记清除法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
从 2012 年起, 所有现代浏览器都使用了标记-清除垃圾回收算法。标记清除法假定存在一个根对象(相当于 js 的全局对象), 垃圾回收器将定期从根对象开始查找, 凡是从根部出发能扫描到的都会保留, 扫描不到的将被回收。
内部流程
- 垃圾收集器找到所有的根, 并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住, 以免将来再次遍历到同一个对象。
- ……如此操作, 直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
几种常见的内存泄漏
-
全局变量
全局变量什么时候需要自动释放内存空间很难判断, 所以在开发中尽量避免使用全局变量, 以提高内存有效使用率。
-
未移除的事件绑定
dom 元素虽然被移除了, 但元素绑定的事件还在, 如果不及时移除事件绑定, 在 IE9 以下版本容易导致内存泄漏。现代浏览器不存在这个问题了, 了解一下即可
-
定时器 setInterval/setTimeout
看下面的一段定时器代码, 一旦我们在其它地方移除了 node 节点, 定时器的回调便失去了意义, 然而它一直在执行导致 callback 无法回收, 进而造成 callback 内部掉数据 resData 也无法被回收。所以我们应该及时 clear 定时器。
let resData = 100; let callback = function () { let node = document.querySelecter(".p"); node && (node.innerHTML = resData); }; setInterval(callback, 1000);
WeakMap、WeakSet
es6 的 WeakMap 和 Map 类似, 都是用于生成键值对的集合, 不同的是 WeakMap 是一种弱引用, 它的键名所指向的对象, 不计入垃圾回收机制, 另外就是 WeakMap 只接受对象作为键名(null 除外), 而 Map 可以接受各种类型的数据作为键。
WeakMap 这种结构有助于防止内存泄漏, 一旦消除对键的引用, 它占用的内存就会被垃圾回收机制释放。WeakMap 保存的这个键值对, 也会自动消失。包括 WeakSet 也是类似的, 内部存储的都是弱引用对象, 不会被计入垃圾回收。
看一个阮一峰 ES6 文档上举的例子:
let myWeakmap = new WeakMap();
myWeakmap.set(document.getElementById("logo"), { timesClicked: 0 });
document.getElementById("logo").addEventListener(
"click",
function () {
let logoData = myWeakmap.get(document.getElementById("logo"));
logoData.timesClicked++;
},
false
);
上面代码中, 我们将 dom 对象作为键名, 每次点击, 我们就更新一下状态。我们将这个状态作为键值放在 WeakMap 里。一旦这个 DOM 节点删除, 该状态就会自动消失, 不存在内存泄漏风险。
WeakSet 和 WeakMap 类似, 它和 set 结构的区别也是两点:
WeakSet 中的对象都是弱引用, 不会被计入垃圾回收
成员只能是对象, 而不能是其他类型的值
所以从垃圾回收的角度来看, 合理的使用 WeakMap 和 WeakSet, 能帮助我们避免内存泄漏。