1

垃圾回收的必要性

由于字符串、对象和数组没有固定大小,所以当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。 ---《JavaScript权威指南(第四版)》

JavaScript的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如:

var a = "before";
var b = "override a";
var a = b; //重写a

这段代码运行之后,“before”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。

垃圾回收原理

最常采用的垃圾回收有两种方法:标记清除、引用计数

标记清除

当执行流入到一个函数中时,会创建该函数的执行环境,执行环境中的变量都会被标记为“进入环境”,从逻辑上讲,永远不能释放“进入执行环境”变量所占用的内存。因为只要执行流入相应的执行环境,就可能会用到这些变量。
垃圾收集器在运行的时候会给存储在内存的中的变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

引用计数

另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

但是用这种方法存在问题:

function problem(){
    var objA = new Object()
    var objB = new Object()
    objA.someOtherObject = objB
    objB.someOtherObject = objA
}

在这个例子中,objA和objB通过各自的属性相互引用;也就是说这两个对象的引用次数都是2。在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,objA和objB还将会继续存在,因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露。

引起内存泄漏的操作

用全局变量缓存数据

将全局变量作为缓存数据的一种方式,将之后要用到的数据都挂载到全局变量上,用完之后也不手动释放内存(因为全局变量引用的对象,垃圾回收机制不会自动回收),全局变量逐渐就积累了一些不用的对象,导致内存泄漏

var x = [];
function grow() {
  x.push(new Array(1000000).join('x'));
  /*
     使用x数组进行某些操作
  */
  setTimeout(grow, 1000);
}
grow()

没有清理的DOM元素引用

(function () {
  var nodes = '';
  var item = {
    // 为了凸显
    name: new Array(1000000).join('x')
  }
  nodes = document.getElementById("nodes")
  nodes.item = item
  nodes.parentElement.removeChild(nodes)
})()

notes变量指向的是页面中的一个元素(也是内存中的一块空间),当将 notes 对应的元素从页面中移除后,其在内存中对应的空间由于仍然由notes变量指向(引用),因此垃圾回收机制不会将这块内存空间回收,从而导致内存泄漏

 被遗忘的定时器或者回调

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

这样的代码很常见, 如果id为Node的元素从DOM中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对someResource的引用, 定时器外面的someResource也不会被释放.

闭包循环引用

var theThing = null  
var replaceThing = function () {
  var originalThing = theThing
  var unused = function () {
    if (originalThing)
      console.log("hi")
  }
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage)
    }
  };
};
setInterval(replaceThing, 1000)

这种内存泄漏的分析请参考here

参考

《JavaScript权威指南》
javascript典型内存泄漏及chrome的排查方法


jhhfft
590 声望40 粉丝

Write the Code. Change the World.