JS垃圾回收机制

浏览器的垃圾回收机制(Garbage collection
),简称GC,它会周期性运行以释放那些不需要的内存,否则,JavaScript的解释器将会耗尽全部系统内存而导致系统崩溃。

具体到浏览器中的实现,通常有两个策略:标记清楚和引用计数。

引用计数法

此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
let car = {
    logo: 'luhu',
    price: 100
}   // car 是第一个对这个对象的引用

let jeep = car // jeep是第二个对这个对象的引用
car.logo = null // logo属性虽然设置为null 但此属性还在被jeep引用 不会被回收
car = '' //  此时这个对象还有有jeep一个引用
jeep = null // jeep设置为null 那个对象是零引用了 可以被回收了

引用计数法是最初级的垃圾收集算法,如果某对象没有其他对象指向它了,那就说明它可以被回收。但是它无法处理循环引用的问题。

循环引用的限制

// 引用计数法的限制
function f(){
  let o1 = {}
  let o2 = {}
  o1.a = o2
  o2.a = o1

  return 100
}

f()

我们执行f函数,它返回了一个数字,和内部的o1,o2没什么关系,但是对引用计数法来说,o1,o2它们之间还存在着相互引用,并不会被回收。这就造成了内存泄漏。

标记清除法

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

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。标记清除法假定存在一个根对象(相当于js的全局对象),垃圾回收器将定期从根对象开始查找,凡是从根部出发能扫描到的都会保留,扫描不到的将被回收。

从根对象开始扫描

右侧的部分无法到达,它将会被回收

就像是一桶水我们把它从根对象泼下去,水流过的地方都没事,没沾上水的对象就该回收了。

内部流程

  1. 垃圾收集器找到所有的根,并“标记”(记住)它们。
  2. 然后它遍历并“标记”来自它们的所有引用。
  3. 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
  4. ……如此操作,直到所有可达的(从根部)引用都被访问到。
  5. 没有被标记的对象都会被删除。

几种常见的内存泄漏

  1. 全局变量

全局变量什么时候需要自动释放内存空间很难判断,所以在开发中尽量避免使用全局变量,以提高内存有效使用率。

  1. 未移除的事件绑定

dom元素虽然被移除了,但元素绑定的事件还在,如果不及时移除事件绑定,在IE9以下版本容易导致内存泄漏。现代浏览器不存在这个问题了,了解一下即可。

let div = document.querySelector(".div");
let name = 'lee'
let handler = function () {
    console.log(name);
}
div.addEventListener('click', handler, false)

div.parentNode.removeChild(div) // 在IE9以下的老版本事件还在
  1. 无效的dom引用

有时候将dom作为对象的key存储起来很有用,但是在不需要该dom时,要记得及时解除对它的引用。

var ele = {
  node: document.getElementById('node')
};

document.body.removeChild(document.getElementById('node')); // 此时ele中还存在对node的引用
  1. 定时器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)

另外单独说一下闭包,闭包和内存泄漏没有半毛钱关系,只是由于IE9之前的版本垃圾收集机制的原因,导致内存无法进行回收,这是IE的问题,现代浏览器基本都不存在这个问题。当然闭包要是使用不当肯定是会造成内存泄漏。

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结构的区别也是两点:

  1. WeakSet中的对象都是弱引用,不会被计入垃圾回收
  2. 成员只能是对象,而不能是其他类型的值

所以从垃圾回收的角度来看,合理的使用WeakMap和WeakSet,能帮助我们避免内存泄漏。

小结

js的垃圾回收机制我们无法人为干预,浏览器会定期巡查,js引擎在内部做了很多优化,使其可以执行的更快,现代浏览器基本都采用标记清楚的方法进行垃圾回收。了解内存泄漏的原因以及如何去规避,防止翻车事故的发生

参考资料: developer.mozilla.orgjavascript.info

你可能感兴趣的:(javascript)