第107题 JS内存泄露如何检测?场景有哪些?

内存泄漏 :当一个对象不再被使用,但是由于某种原因,它的内存没有被释放,这就是内存泄漏。

垃圾回收机制

  • 对于在JavaScript中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当JavaScript的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。
  • 内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
  • JavaScript有自己的一套垃圾回收机制,JavaScript的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
  • 针对JavaScript的垃圾回收机制有以下两种方法(常用):标记清除(现代),引用计数(之前)

有两种垃圾回收策略:

  • 标记清除 :标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数 :它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收

标记清除的缺点:

  • 内存碎片化 ,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
  • 分配速度慢 ,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。

解决以上的缺点可以使用 标记整理(Mark-Compact)算法 标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)

引用计数的缺点:

  • 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
  • 解决不了循环引用导致的无法回收问题
    • IE 6、7JS对象和DOM对象循环引用,清除不了,导致内存泄露

V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。

  • 针对新生区采用并行回收。
  • 针对老生区采用增量标记与惰性回收

注意闭包不是内存泄露,闭包的数据是不可以被回收的

拓展:WeakMap、WeakMap的作用

  • 作用是防止内存泄露的
  • WeakMapWeakMap的应用场景
    • 想临时记录数据或关系
    • vue3中大量使用了WeakMap
  • WeakMapkey只能是对象,不能是基本类型

如何检测内存泄露

内存泄露模拟

    <p>
      memory change
      <button id="btn1">start</button>
    </p>
    
    <script>
        const arr = []
        for (let i = 0; i < 10 * 10000; i++) {
          arr.push(i)
        }
    
        function bind() {
          // 模拟一个比较大的数据
          const obj = {
            str: JSON.stringify(arr) // 简单的拷贝
          }
    
          window.addEventListener('resize', () => {
            console.log(obj)
          })
        }
    
        let n = 0
        function start() {
          setTimeout(() => {
            bind()
            n++
    
            // 执行 50 次
            if (n < 50) {
              start()
            } else {
              alert('done')
            }
          }, 200)
        }
    
        document.getElementById('btn1').addEventListener('click', () => {
          start()
        })
    </script>

打开开发者工具,选择 Performance,点击 Record,然后点击 Stop,在 Memory 选项卡中可以看到内存的使用情况。

内存泄露的场景(Vue为例)

  • 被全局变量、函数引用,组件销毁时未清除
  • 被全局事件、定时器引用,组件销毁时未清除
  • 被自定义事件引用,组件销毁时未清除
    <template>
      <p>Memory Leak Demo</p>
    </template>
    
    <script>
    export default {
      name: 'Memory Leak Demo',
      data() {
        return {
          arr: [10, 20, 30], // 数组 对象
        }
      },
      methods: {
        printArr() {
          console.log(this.arr)
        }
      },
      mounted() {
        // 全局变量
        window.arr = this.arr
        window.printArr = ()=>{
          console.log(this.arr)
        }
    
        // 定时器
        this.intervalId = setInterval(() => {
          console.log(this.arr)
        }, 1000)
    
        // 全局事件
        window.addEventListener('resize', this.printArr)
        // 自定义事件也是这样
      },
      // Vue2是beforeDestroy
      beforeUnmount() {
        // 清除全局变量
        window.arr = null
        window.printArr = null
    
        // 清除定时器
        clearInterval(this.intervalId)
    
        // 清除全局事件
        window.removeEventListener('resize', this.printArr)
      },
    }
    </script>

拓展 WeakMap WeakSet

weakmapweakset 都是弱引用,不会阻止垃圾回收机制回收对象。

    const map = new Map() 
    function fn1() {
      const obj = { x: 100 }
      map.set('a', obj) // fn1执行完 map还引用着obj
    }
    fn1()
    const wMap = new WeaMap() // 弱引用
    function fn1() {
      const obj = { x: 100 }
      // fn1执行完 obj会被清理掉
      wMap.set(obj, 100) // weakMap 的 key 只能是引用类型,字符串数字都不行
    }
    fn1()
Last Updated:
Contributors: leeguooooo