理解JavaScript(6):JavaScript垃圾回收机制和内存泄漏

时间:2021-09-05 20:56:51

JavaScript垃圾回收机制

  JavaScript具有自动垃圾回收机制(GC:Garbage Collection),也就是说,执行环境会负责管理代码执行过程中的内存。其实质就是垃圾回收器会周期性的找出那些不再继续使用的变量,释放其内存。标记无用变量的方法通常有两种,标记清除和引用计数。

  • 标记清除
    这是JavaScript使用最常见的垃圾回收机制,当变量进入环境时将其标记为”进入环境”;当变量离开环境时,将其标记为”离开环境”。至于怎么标记有很多种方式,比如特殊位的反转、维护一个列表等等。原则上讲不能够释放进入环境的变量所占的内存,它们随时可能被用到。使用闭包时有些变量不会被清除,可能会造成内存泄漏。
function add () {
    var a = 0;
    var b = 1;
    console.log(a + b);
}
add()
// 执行add函数时,a,b被标记为'进入环境',执行完毕离开环境,并且变量add就是进入环境的变量。
  • 引用计数
    另一种不太常用的垃圾回收机制就是引用计数,引用计数的含义就是跟踪记录每个值被引用的次数。当声明一个变量并将引用类型的值赋值给该变量时,这个值的引用次数就加1。相反,如果包含对这个值引用的变量取得了另外一个值的时候则值的引用次数就减1.单这个引用次数变为0时,则说明没有办法访问这个值了,因而可以将其所占的空间收回。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

    看起来也不错的方式,为什么很少有浏览器采用,还会带来内存泄露问题呢?低版本的IE内存泄漏问题,主要是使用了这种垃圾回收机制。这种方式没办法解决循环引用问题。比如对象A有一个属性指向对象B,而对象B也有有一个属性指向对象A,这样相互引用
function test () {
    var a = {};
    var b = {};
    a.name = b;
    b.name = a;
}

复制代码
这样a和b的引用次数都是2,即使在test()执行完成后,两个对象都已经离开环境,在标记清除的策略下是没有问题的,离开环境的就被清除,但是在引用计数策略下不行,因为这两个对象的引用次数仍然是2,不会变成0,所以其占用空间不会被清理,如果这个函数被多次调用,这样就会不断地有空间不会被回收,造成内存泄露。

内存泄漏

不再用到的内存,不能及时释放,就叫内存泄漏

  • 浏览器判断是否发生内存泄漏

    那么怎样可以观察到内存泄漏呢?

    经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。在浏览器中实时查看内存的方法。
    1. 打开开发者工具,选择 Timeline 面板
    2. 在顶部的Capture字段里面勾选 Memory
    3. 点击左上角的录制按钮。
    4. 在页面上进行各种操作,模拟用户的使用情况。
    5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。
  • 内存泄漏的原因

    意外的全局变量引起的泄漏
function test () {
    string = 'test'; // 函数中未使用var关键字声明的变量
    this.number = 0; // 方法中的this指向window对象
}
test();
console.log(string, number); // test 0
// 解决方法,良好的编程习惯^_^!

  闭包引起的泄漏

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

这个代码片段做了一件事:每次调用 replaceThing 时,theThing 都会获得一个新对象,它包含一个大的数组和一个新的闭包(someMethod)。同时,变量 unused 保留了一个拥有 originalThing 引用的闭包(前一次调用 theThing 赋值给了 originalThing)。已经有点混乱了吗?重要的是,一旦一个作用域被创建为闭包,那么它的父作用域将被共享。

在这个例子中,创建闭包 someMethod 的作用域是于 unused 共享的。unused 拥有 originalThing 的引用。尽管 unused 从来都没有使用,但是 someMethod 能够通过 theThing 在 replaceThing 之外的作用域使用(例如全局范围)。并且由于 someMethod 和 unused 共享 闭包范围,unused 的引用将强制保持 originalThing 处于活动状态(两个闭包之间共享整个作用域)。这样防止了垃圾回收。
当这段代码重复执行时,可以观察到内存使用量的稳定增长。当 GC 运行时,也没有变小。实质上,引擎创建了一个闭包的链接列表(root 就是变量 theThing),并且这些闭包的作用域中每一个都有对大数组的间接引用,导致了相当大的内存泄漏

  当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。

<div id="myDiv">
    <input type="button" value="Click me" id="myBtn">
</div>
<script type="text/javascript"> var btn = document.getElementById("myBtn"); btn.onclick = function(){ document.getElementById("myDiv").innerHTML = "Processing..."; } var myDiv = document.getElementById("myDiv"); // 加上 btn.onclick = null; 防止内存泄漏 myDiv.removeChild(btn); </script>

子元素存在引用引起的内存泄漏

<div id="A">
    <div id="B">
        <div id="C"></div>
    </div>
</div>
<script type="text/javascript"> var A = document.getElementById("A"); var B = document.getElementById("B"); var C = document.getElementById("C"); A.removeChild(B); // 只会断开B与dom的链接,由于C保留对B的间接引用,B还是会存在,手动清除防止内存泄漏 B = null; C = null;

被遗忘的定时或者回调导致内存泄漏

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

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

解决方法:在观察者模式下,重要的是在他们不再被需要的时候显式地去删除它们(或者让相关对象变为不可达)。