• home > webfront > ECMAS > emphasis >

    再谈JavaScript垃圾回收机制:分析与排查JS内存泄露情形

    Date:

    JavaScript不需要像C一样手动管理内存,JavaScript的内存管理

    首先推广下姊妹篇:《再谈js对象数据结构底层实现原理-object array map set 》、《Chrome开发工具Memory页面渲染内存占用分析

    JavaScript垃圾回收机制

    首先JavaScript是一个有Garbage Collection 的语言,也就是我们不需要手动的回收内存。js的垃圾回收机制分为

    • 引用计数(reference counting):机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收。

      该方式会引起内存泄漏的原因是它不能解决循环引用的问题: var a={};var b={};a.prop = b;b.prop = a;

    • 标记清除(mark and sweep):大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。标记方式不定,可以是某个特殊位的反转或维护一个列表等。

      垃圾收集器给内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上的标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量。

      JavaScript内存管理JavaScript内存管理 堆栈

    不同的JavaScript引擎有不同的垃圾回收机制,这里我们主要以V8这个被广泛使用的JavaScript引擎为主——标记清除法Mark-and-sweep。

    JavaScript内存分配和回收的关键词:GC根、作用域

    GC根:一般指全局且不会被垃圾回收的对象,比如:window、document或者是页面上存在的dom元素。JavaScript的垃圾回收算法会判断某块对象内存是否是GC根可达(存在一条由GC根对象到该对象的引用),如果不是那这块内存将会被标记回收

    作用域:在JavaScript的作用域里,我们能够新建对象来分配内存。比如说调用函数,函数执行的过程中就会创建一块作用域,如果是创建的是作用域内的局部对象,当作用域运行结束后,所有的局部对象(GC根无法触及)都会被标记回收,在JavaScript中能引起作用域分配的有函数调用、with和全局作用域。

    作用域的分类:全局作用域、局部作用域、闭包作用域

    全局作用域:每个JavaScript进程都会有一个全局作用域,全局作用域上的引用的对象都是常驻内存的,直到进程退出内存才会自动释放

    局部作用域:函数调用会创建局部作用域,在局部作用域中的新建的对象,如果函数运行结束后,该对象没有作用域外部的引用,那该对象将会标记回收

    闭包作用域闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量。闭包对象是当前作用域中的所有内部函数作用域共享的,并且这个当前作用域的闭包对象中除了包含一条指向上一层作用域闭包对象的引用外,其余的存储的变量引用一定是当前作用域中的所有内部函数作用域中使用到的变量。

    var closure = (function(){    //这里是闭包的作用域
        var i = 0 // i就是自由变量
        return function(){        console.log(i++)
        }
    })()

    闭包作用域会保持对自由变量的引用。上面代码的引用链就是:window -> closure -> i

    闭包会使变量始终保存在内存中,如果不当使用会增大内存消耗

    javascript的垃圾回收原理

    手动释放全局作用域上的引用的对象有两种方式:

    1. 在javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收; 

    2. 如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。

    js内存泄露分析

    全局变量造成内存不被回收

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

    function foo(arg) { 
        bar = "this is a hidden global variable"; //等同于window.bar="this is a hidden global variable"
        this.bar2= "potential accidental global";//这里的this 指向了全局对象(window),等同于window.bar2="potential accidental global"}

    解决方法:在JavaScript程序中添加,开启严格模式'use strict',可以有效地避免上述问题。

    注意:那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓 存内容无法被回收。

    闭包循环引用

    闭包在IE6下会造成内存泄漏,但是现在已经无须考虑了。值得注意的是闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏(闭包内存不回收,占用过多内存)。闭包会造成对象引用的生命周期脱离当前函数的上下文,如果闭包如果使用不当,可以导致环形引用(circular reference),类似于死锁,只能避免,无法发生之后解决,即使有垃圾回收也还是会内存泄露。  

    (function(){
      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 someMethod() {
            console.log('someMessage')
          }
        };
      };
      setInterval(replaceThing,100)
    })()

    没有及时清理的计时器或回调函数

    js中常用的定时器setInterval()、setTimeout().他们都是规定延迟一定的时间执行某个代码,而其中setInterval()和链式setTimeout()在使用完之后如果没有手动关闭,会一直存在执行占用内存,所以在不用的时候我们可以通过clearInterval()、clearTimeout() 来关闭其对应的定时器,释放内存。

    setInterval用多了,会占用大量的内存。因此setInterval我们必须及时清理!可以用如下方式清理setInterval。

    function b() {
      var a = setInterval(function() {
        console.log("Hello");
        clearInterval(a);
        b();
      }, 50);
    }
    b();

    或者

    function init()
    {
      window.ref = window.setInterval(function() { draw(); }, 50);
    }
    function draw()
    {
      console.log('Hello');
      clearInterval(window.ref);
      init();
    }
    init();

    或者我们用setTimeout

    function time(f, time) {
      return function walk() {
        clearTimeout(aeta);
        var aeta =setTimeout(function () {
          f();
          walk();
        }, time);
      };
    }
    
    time(updateFormat, 1000)();

    DOM元素相关的泄露

    游览器中DOM和js采用的是不一样的引擎,DOM采用的是渲染引擎,而js采用的是v8引擎,所以在用js操作DOM时会比较耗费性能,因为他们需要桥来链接他们。为了减少DOM的操作,我们一般将常用的DOM采用变量引用的方式会将其缓存在当前环境。如果在进行一些删除、更新操作之后,可能会忘记释放已经缓存的DOM,话不多说直接来个例子

    没有清理的DOM元素引用

    var refA = document.getElementById('refA');
    document.body.removeChild(refA);
    // #refA不能回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。

    解决办法:refA = null;

    给DOM对象添加的属性是一个对象的引用:

    var MyObject = {}; 
    document.getElementById('myDiv').myProp = MyObject;

    解决方法: 在window.onunload事件中写上: document.getElementById('myDiv').myProp = null;  

    DOM对象与JS对象相互引用:

    function Encapsulator(element) {
      this.elementReference = element;
      element.myProp = this;
    }
    new Encapsulator(document.getElementById('myDiv'));

    解决方法: 在onunload事件中写上: document.getElementById('myDiv').myProp = null;   

    给DOM对象用addEventListener||attachEvent绑定事件:

    function doClick() {} 
    element.attachEvent("onclick", doClick);

    解决方法: 在onunload事件中写上: element.detachEvent('onclick', doClick);   

    从外到内执行appendChild。这时即使调用removeChild也无法释放:

    var parentDiv = document.createElement("div"); 
    var childDiv = document.createElement("div"); 
    document.body.appendChild(parentDiv); 
    parentDiv.appendChild(childDiv);

    解决方法: 从内到外执行appendChild:   

    var parentDiv = document.createElement("div"); 
    var childDiv = document.createElement("div"); 
    parentDiv.appendChild(childDiv); 
    document.body.appendChild(parentDiv);


    echart maptalks等组件不停调用导致内存泄露

    不停的用setInterval ajax调用echart maptalks,更新echart表格及地图数据,即使清理了setInterval,也会导致内存泄露!

    解决办法,首先及时清理:

    myChart.clear();
    myChart.setOption(option);


    参考文章:

    javascript内存泄露及谷歌浏览器查看内存使用 https://www.haorooms.com/post/javascript_neicun_use

    javascript典型内存泄漏及chrome的排查方法 https://segmentfault.com/a/1190000008901861

    js常见的内存泄漏及解决方法总汇 www.fly63.com/article/detial/225?type=2




    转载本站文章《再谈JavaScript垃圾回收机制:分析与排查JS内存泄露情形》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2016_0219_7612.html