JS内存空间

一步一步重温 JavaScript 基础系列。
本章节将会理解JS的内存空间、垃圾回收机制、内存泄漏以及如何分析定位内存泄漏

内存空间

堆与栈

  • 栈内存,结构特点是后进先出(LIFO)
  • 堆内存,结构类似于书架,通过 key-value 的形式读取

变量的存放

  • 基本类型,保存在栈内存中,占据内存空间大小固定,通过按值来访问
  • 引用类型,其值大小不固定,保存在堆内存中,但其引用大小固定,保存在栈内存中。当查询引用类型的变量时,先从栈中读取内存地址,然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问

可以通过复制变量的值来理解如下:

复制变量的值图解

内存空间管理

内存空间生命周期

  • 分配所需的内存
  • 使用分配的内存(读、写)
  • 不需要时将其释放

垃圾回收机制

JS有自动垃圾收集机制GC(Garbage collection),因为开销较大,GC并非实时进行,而是定时周期性执行。所以,通过将变量赋值为null等方法将变量从当前执行环境释放时,并不会被立即回收清除。
在局部作用域中,当函数执行完毕后,执行栈就会将其推出,内部变量很容易被标记并回收。但是全局作用域以及闭包内的变量,什么时候释放回收,JS引擎很难判断,因此,在实际开发中,应尽量减少使用全局变量和闭包。

  • 回收方式一:标记清除

    • 当变量进入执行环境时,标记为“进入环境”,离开执行环境时,标记为“离开”;
    • 垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式);
    • 然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包);
    • 而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了;
    • 最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
  • 回收方式二:引用计数

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

内存泄漏

内存溢出,是指程序运行所需要的内存,超过剩余内存,常见如递归死循环等
内存泄漏,是指不再用到的内存,没有及时释放。

引起内存泄漏的几种情况

  • DOM元素的不恰当处理

    • 原因:DOM元素被删除,但是仍然有变量引用该DOM,导致GC无法回收该变量
    • 解决:DOM元素被删除时,将引用该DOM的变量,赋值为null,解除引用,待GC回收清除
    1
    2
    3
    4
    5
    6
    7
    8
    // 声明一个变量,保存DOM元素video
    var video = document.getElmentById('#video');

    // 将video从body中移除
    document.querySelector('body').removeChild(video);

    console.log(video);
    // 打印结果仍然为DOM元素video
  • 意外的全局变量

    • 原因:在函数作用域内,忘记使用var声明变量,或者是在this上新增属性,导致意外创建一个全局变量
    • 解决:使用严格模式 ‘use strict’,避免此类错误发生
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo () {
    // 忘记使用var声明变量,此时name成为全局变量
    name = 'xug';

    // 在this上新增age属性,由于foo由全局调用,此时this指向全局作用域window,故age成为全局作用域
    this.age = 32;
    }

    foo()
    // 如以上所示,即便foo被GC回收,name和age也不会被回收,由此造成内存泄漏
  • 被遗忘的计时器或回调函数

    • 原因:setInterval()和 setTimeout()在使用完之后如果没有手动清除,会一直存在执行,占用内存
    • 解决:定时器手动清除,且将引用定时器的变量值置为null
      例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // setInterval
      var timer1 = setInterval(function(){
      console.log('111')
      },1000)

      clearInterval(timer1)
      timer1 = null


      // setTimeout
      var timer2 = setTimeout(function(){
      console.log('222')
      },1000)

      clearTimeout(timer2)
      timer2 = null
  • 闭包

    • 原因:形成闭包后,内层函数会引用外层函数的变量,这样导致JS引擎无法回收此变量
    • 解决:解除对匿名函数的引用
      例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function fn () {
      var num = 3;
      return function () {
      var n = 0;
      console.log(++n);
      console.log(++num);
      }
      }

      var fn1 = fn();
      fn1() // 1 4
      fn1() // 1 5

    一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,并且匿名函数内部引用着fn里的变量num,所以变量num无法被GC回收,由此产生内存泄漏。而变量n是每次被调用时新创建的,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁。

    只需将 fn1 = null, 即可释放对匿名函数的引用

如何分析内存泄漏

  • window.performance.memory 输出当前内存属性

    • jsHeapSizeLimit: 内存大小限制
    • totalJSHeapSize: 可使用的内存
    • usedJSHeapSize: JS对象(包括V8引擎内部对象)已占用的内存
      1
      2
      3
      4
      5
      6
      window.performance.memory
      MemoryInfo {
      jsHeapSizeLimit: 4294705152,
      totalJSHeapSize: 101622250,
      usedJSHeapSize: 86986030
      }
  • Chrome浏览器,录制 Performance-memory 查看各项指标变化

    • JS Heap: 应用的内存占用量
    • Documents: 当前页面中使用的样式或者脚本文件数目
    • Nodes: 内存中 DOM 节点数目
    • Listeners: 当前页面上注册的 JavaScript 事件监听器数量
      如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。反之,则存在内存泄漏。
  • 利用node命令行:process.memoryUsage(), 输出内存使用情况

    • rss(resident set size):所有内存占用,包括指令区和堆栈。
    • heapTotal:”堆”占用的内存,包括用到的和没用到的。
    • heapUsed:用到的堆的部分。
    • external: V8 引擎内部的 C++ 对象占用的内存
原创技术分享,您的支持将鼓励我继续创作