js常见内存问题分析

193 阅读3分钟

内存爆掉的页面

输入图片说明

内存泄漏(Memory leak)

定义:当进程不再需要某些内存的时候,这些不再被需要的内存依然没有(不能)被进程回收

js中常见的内存溢出

意外的全局变量

function foo() {
  bar = 1
}
function foo() {
  bar = [...Array(1000000).keys()]
}
foo()

setTimeout(() => {
  bar = null
}, 5000);

输入图片说明 输入图片说明

错误的闭包写法造成

function fn () {
  var a = 'a'
  return function () {
    console.log(a)
  };
}

const bar = fn()
bar()
function foo() {
  var temp_object = new Object()
  temp_object.x = 1
  temp_object.y = 2
  temp_object.array = [...Array(1000000).keys()]

  const temp = temp_object.x
  return function () {
    console.log(temp)
  }
}

const bar = foo()
bar()
function foo() {
  var temp_object = new Object()
  temp_object.x = 1
  temp_object.y = 2
  temp_object.array = [...Array(1000000).keys()]

  return function () {
    console.log(temp_object.x)
  }
}

const bar = foo()
bar()

输入图片说明 输入图片说明 输入图片说明 输入图片说明

未删除的js dom

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button id="create">新增</button>
    <script>
      var detachedTree;

      function create() {
        var ul = document.createElement('ul');
        for (var i = 0; i < 10; i++) {
          var li = document.createElement('li');
          ul.appendChild(li);
        }
        detachedTree = ul;
      }

      document.getElementById('create').addEventListener('click', create);
    </script>
  </body>
</html>

输入图片说明

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button id="rmListNode">删除列表</button>
    <button id="toNull">删除js引用</button>
    <script>
      let detachedUlTree = document.createElement('ul')
      for (let i = 0; i < 30000; i++) {
        let li = document.createElement('li')
        li.innerText = i
        detachedUlTree.appendChild(li)
      }
      document.body.appendChild(detachedUlTree)

      document.querySelector('#rmListNode').addEventListener('click', function() {
        document.body.removeChild(detachedUlTree)
      })

      document.querySelector('#toNull').addEventListener('click', function() {
        document.body.removeChild(detachedUlTree)
        detachedUlTree = null
      })
    </script>
  </body>
</html>

输入图片说明 输入图片说明

被遗忘的定时器

setInterval(function () {
  var renderer = document.getElementById('renderer')
  if (renderer) {
    // do something
  }
}, 5000)

document.querySelector('btn').addEventListener('click', function() {
  document.body.removeChild(document.getElementById('renderer'))
  // 应该把定时器保存成变量,在这里用clearInterval清理掉
})
<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
      // 获取一些数据
    },
  },
  mounted() {
    setInterval(function() {
      // 轮询获取数据
      this.refresh()
    }, 2000)
  },
}
</script>

组件销毁后未删除定时器,定时器还在定时执行,正确的写法

<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
    },
  },
  mounted() {
    this.refreshInterval = setInterval(function() {
      this.refresh()
    }, 2000)
  },
  beforeDestroy() {
    clearInterval(this.refreshInterval)
  },
}
</script>

被遗忘的事件监听

var renderer = document.getElementById('renderer')
function callBack() {
  var $btn = document.querySelector('btn')
  if ($btn) {
    console.log($btn.innerHTML())
  }
}
renderer.addEventListener('click', callBack)

document.querySelector('deleteBtn').addEventListener('click', function() {
  document.body.removeChild(document.getElementById('btn'))
  // renderer.removeEventListener(callBack)
})
<script> 
 export default {
     mounted () {
      window.addEventListener('resize', this.func)
  }
} 
</script>

组件销毁时应删除时间绑定

mounted () {
  window.addEventListener('resize', this.func)
},
beforeDestroy () {
  window.removeEventListener('resize', this.func)
}

错误的使用map set等(set、map、weakMap、weakSet区别)

key不会被垃圾回收

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
key = null;

正确的写法:

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

map.delete(key);
key = null;

let map = new WeakMap();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

key = null;

输入图片说明 输入图片说明

浏览器内存问题分析思路:

  1. 借助性能分析工具Performance、performance monitor分析主要问题
  2. 使用Memory定位具体的问题点

相关的重要概念

Shallow size(浅层大小)和 Retained size(保留的大小)(参考

  • 这里是列表文本浅层大小:对象本身的大小
  • 这里是列表文本保留的大小:在删除对象本身及其从GC根不可达的依赖对象时释放的内存大小

GC roots

在浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):

  • 全局的 window 对象(位于每个 iframe 中)
  • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成
  • 存放调用栈上变量

调用栈

function foo() {
  var a = '极客时间'
  var b = a
  var c = { name: '极客时间' }
  var d = c
  debugger
}
foo()

输入图片说明 输入图片说明

垃圾回收算法(标记清除、V8实现):

  1. 这里是列表文本通过 GC Roots 标记空间中活动对象和非活动对象。通过 GC Root 遍历到的对象,我们就认为该对象是可访问的(reachable),是活动对象;通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),那么这些不可访问的对象就可能被回收
  2. 回收非活动对象所占据的内存
  3. 做内存碎片整理

此处简单介绍,详细过于复杂,感兴趣可自行学习极客时间图解google V8第20、21、22小节

参考文档

图解 Google V8
浏览器工作原理与实践
google devtools doc:Fix memory problems
使用 chrome-devtools Memory 面板
万恶的前端内存泄漏及万善的解决方案
深入了解 JavaScript 内存泄露
你不知道的 WeakMap
原来 JavaScript 中的 WeakMap 是这样子的