JS的内存管理

128 阅读5分钟

内存的生命周期

image.png

  1. 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:变量、函数执行时
  3. 内存回收:不需要时将其释放、归还

垃圾回收算法

对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用,然后怎么清除它的一个方法

  1. 引用计数(现代浏览器不再使用)

引擎会有张引用表,保存了内存里面的资源的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

优点

  • 发现垃圾立即回收
  • 最大限度的减少程序暂停(当内存快满的时候引用计数找到引用数为0的内存空间立即释放)

缺点

  • 无法回收循环变量
  • 时间开销大(需要时刻监控着当前对象的引用数值是否需要修改,当彼岸来给你很多时,时间开销就会比较大)
  1. 标记清除(常用)

      工作流程:

    1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
    2. 从根部出发将能触及到的对象的标记清除。
    3. 那些还存在标记的变量被视为准备删除的变量。
    4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

      优点:

    • 相对于引用计数的方法,可解决循环引用对象的问题

      缺点:

    • 标记清除算法的空间回收,地址不连续会导致空间碎片化
    • 不会立即回收垃圾对象(清除的时候是停止工作的)
  1. V8中的垃圾回收

      V8把堆内存分成了两部分进行处理:

    1. 新生代存储区,即临时分配的内存,存活时间短

      大小: 在64位和32位的系统下分别为32MB和16MB

      工作流程:

    1. 新生代空间被一分为二,其中From部分表示正在使用的内存,To是目前闲置的内存。
    2. 当垃圾回收时,V8将From部分的对象检查一遍,如果存活就复制到To内存中,如果是非存活对象直接回收
    3. 当From中的对象被处理完成之后,From和To的角色对调,From为闲置,To为正在使用,如此循环
    4. from,To这两个空间来回倒腾是为了处理内存碎片的问题

    1. 新生代的“晋升”

      1. 变量在新生代已经经历过一次内存回收
      2. To(闲置)空间的内存占用超过25%
    2. 老生代的内存是常驻内存,存活时间长

      1.     大小: 在64位和32位的系统下分别为1.4G和700M

      2.     工作流程:

      3. 第一步:采用标记清除的方法,但使用这种方式会造成内存碎片,又是怎么处理的呢

      4. 第二步:整理内存碎片,v8解决方式非常简单粗暴,在清除阶段结束后,把存活的对象全部往一边靠拢,就和上图差不多,这一部分是非常耗时的

      5. 由于以上方式非常耗时,v8采取了增量标记的方式,即一口气的将标记任务分成很多小的部分,每完成一部分就让js应用执行一会儿,然后再回收,如此循环,知道处理完成,过程类似fiber的机制。

内存泄漏

内存泄漏,指在JS中已经分配内存地址的对象由于长时间未进行内存释放或无法清除,造成了长期占用内存,使得内存资源浪费,最终导致运行的应用响应速度变慢以及最终崩溃的情况。

内存泄漏的常见情况

  1. 闭包

有权访问另一个函数作用域中的变量的函数

  1. 隐式全局变量

    1.   函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。但是对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以全局变量通常不会被回收
    2. function fn(){
        // 没有声明从而制造了隐式全局变量test1
        test1 = new Array(1000).fill('isboyjc1')
        
        // 函数内部this指向window,制造了隐式全局变量test2
        this.test2 = new Array(1000).fill('isboyjc2')
      }
      fn()
      
  2. ****DOM 引用

    1. <div id="root">
        <ul id="ul">
          <li></li>
          <li></li>
          <li id="li3"></li>
          <li></li>
        </ul>
      </div>
      <script>
        let root = document.querySelector('#root')
        let ul = document.querySelector('#ul')
        let li3 = document.querySelector('#li3')
        
        // 由于ul变量存在,整个ul及其子元素都不能GC
        root.removeChild(ul)
        
        // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
        ul = null
        
        // 已无变量引用,此时可以GC
        li3 = null
      </script>
      
  3. 定时器

    1.   当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout来清除,另外,浏览器中的 requestAnimationFrame 也存在这个问题,我们需要在不需要的时候用 cancelAnimationFrame API 来取消使用
  4. 事件监听

    1. <template>
        <div></div>
      </template>
      
      <script>
      export default {
        created() {
          window.addEventListener("resize", this.doSomething)
        },
        beforeDestroy(){
          window.removeEventListener("resize", this.doSomething)
        },
        methods: {
          doSomething() {
            // do something
          }
        }
      }
      </script>
      
  5. 事件的监听发布

    1.   当实现了监听者模式并收集了相关的事件处理函数,其中引用的变量或者函数都被认为是需要的而不会进行回收
    2. export default {
        created() {
          eventBus.on("test", this.doSomething)
        },
        beforeDestroy(){
          eventBus.off("test", this.doSomething)
        },
        methods: {
          doSomething() {
            // do something
          }
        }
      }
      </script>
      
  6. 未清理的console

    1.   之所以在控制台能看到数据输出,是因为浏览器保存了对输出对象的信息数据引用,也正是因此未清理的 console 如果输出了对象也会造成内存泄漏