防止内存泄漏之weakMap

  什么是内存泄漏?

  对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

  不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

  垃圾回收机制

  最常使用的方法叫做“引用计数”(reference counting):语言引擎有一张”引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

  如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

  内存泄漏的识别方法

  经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。

  • 浏览器    
    • 打开开发者工具,选择 Timeline 面板
    • 在顶部的Capture字段里面勾选 Memory
    • 点击左上角的录制按钮。
    • 在页面上进行各种操作,模拟用户的使用情况。
    • 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。
  • 命令行
    • 命令行可以使用 Node 提供的process.memoryUsage方法
    • 该对象包含四个字段,单位是字节,含义如下:
      • rss(resident set size):所有内存占用,包括指令区和堆栈。
      • heapTotal:”堆”占用的内存,包括用到的和没用到的。
      • heapUsed:用到的堆的部分。
      • external: V8 引擎内部的 C++ 对象占用的内存。
  • WeakSet  
  1. WeakSet 的成员只能是对象,而不能是其他类型的值
  2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
  3. 实例:
  • 栈(stack)堆(heap)
    • stack为自动分配的内存空间,它由系统自动释放;
    • 而heap则是动态分配的内存,大小不定也不会自动释放。
  • 基本类型和引用类型
    • 基本类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配。

        5种基本数据类型有Undefined、Null、Boolean、Number 和 String,它们是直接按值存放的,所以可以直接访问。

    • 引用类型:存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样,要根据情况来进行特定的分配。

    • 当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。


  • 传值与传址
    • 基本类型与引用类型最大的区别实际就是传值与传址的区别
    •   var a = [1,2,3,4,5];
          var b = a;
          var c = a[0];
          alert(b);//1,2,3,4,5
          alert(c);//1
          //改变数值        
          b[4] = 6;
          c = 7;
          alert(a[4]);//6
          alert(a[0]);//1

      从上面我们可以得知,当我改变b中的数据时,a中数据也发生了变化;但是当我改变c的数据值时,a却没有发生改变。

        这就是传值与传址的区别。因为a是数组,属于引用类型,所以它赋予给b的时候传的是栈中的地址(相当于新建了一个不同名“指针”),而不是堆内存中的对象。而c仅仅是从a堆内存中获取的一个数据值,并保存在栈中。所以b修改的时候,会根据地址回到a堆中修改,c则直接在栈中修改,并且不能指向a堆内存中。

  • 浅拷贝
    • 前面已经提到,在定义一个对象或数组时,变量存放的往往只是一个地址。当我们使用对象拷贝时,如果属性是对象或数组时,这时候我们传递的也只是一个地址。因此子对象在访问该属性时,会根据地址回溯到父对象指向的堆内存中,即父子对象发生了关联,两者的属性值会指向同一内存空间。
    • var a = {
              key1:"11111"
          }
          function Copy(p) {
              var c = {};
              for (var i in p) { 
                c[i] = p[i];
              }
              return c;
        }
          a.key2 = ['小辉','小辉'];
          var b = Copy(a);
         b.key3 = '33333';
          alert(b.key1);     //1111111
          alert(b.key3);    //33333
          alert(a.key3);    //undefined

      a对象中key1属性是字符串,key2属性是数组。a拷贝到b,12属性均顺利拷贝。给b对象新增一个字符串类型的属性key3时,b能正常修改,而a中无定义。说明子对象的key3(基本类型)并没有关联到父对象中,所以undefined。

    • b.key2.push("大辉");
      alert(b.key2);    //小辉,小辉,大辉
      alert(a.key2);    //小辉,小辉,大辉

      但是,若修改的属性变为对象或数组时,那么父子对象之间就会发生关联。从以上弹出结果可知,我对b对象进行修改,a、b的key2属性值(数组)均发生了改变。其在内存的状态,可以用下图来表示。

    • 原因是key1的值属于基本类型,所以拷贝的时候传递的就是该数据段;但是key2的值是堆内存中的对象,所以key2在拷贝的时候传递的是指向key2对象的地址,无论复制多少个key2,其值始终是指向父对象的key2对象的内存空间。
  • 深拷贝
    • 或许以上并不是我们在实际编码中想要的结果,我们不希望父子对象之间产生关联,那么这时候可以用到深拷贝。既然属性值类型是数组和或象时只会传址,那么我们就用递归来解决这个问题,把父对象中所有属于对象的属性类型都遍历赋给子对象即可。测试代码如下:
      •   
        function Copy(p, c) {
                var c = c || {};
                for (var i in p) {
                  if (typeof p[i] === 'object') {
                     c[i] = (p[i].constructor === Array) ? [] : {};
                     Copy(p[i], c[i]);
                  } else {
                     c[i] = p[i];
                  }
                }
                return c;
          }    
            a.key2 = ['小辉','小辉'];
            var b={};
            b = Copy(a,b);        
            b.key2.push("大辉");
            alert(b.key2);    //小辉,小辉,大辉
            alert(a.key2);    //小辉,小辉

        由上可知,修改b的key2数组时,没有使a父对象中的key2数组新增一个值,即子对象没有影响到父对象a中的key2。其存储模式大致如下:

posted @ 2017-04-25 10:59  Caraxiong  阅读(244)  评论(0编辑  收藏  举报