内存泄漏

内存泄漏

页面偶现卡死、崩溃,特别是打开页面较久的时候发生概率较高。打开任务管理器,看到内存占有率已经很高了,初步判断可能存在内存泄漏的情况。下面排查内存泄漏的原因。

系统进程不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。当内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。Chrome 限制了浏览器所能使用的内存极限。

js 复制代码
// 当前页面所能使用的最大堆内存,超过页面崩溃报内存错误,以字节计算。
performance.memory.jsHeapSizeLimit

// 已分配的堆体积,以字节计算。
performance.memory.totalJSHeapSize

// 当前 JS 堆活跃段(segment)的体积,以字节计算。
performance.memory.usedJSHeapSize

垃圾回收 GC

标记清除(JavaScript中主流的垃圾回收算法)

JS垃圾回收机制:当一个变量没有被其他变量或属性引用的时候,认定为垃圾,堆会被释放。

当变量进入执行环境是,就标记这个变量为"进入环境"。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为"离开环境"(例如:一段script脚本、vue实例中的script标签内容等)

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉

在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

引用计数

语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

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

arduino 复制代码
const arr = [1, 2, 3, 4]
console.log('hello world')

下面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存

如果需要这块内存被垃圾回收机制释放,只需要设置如下:

ini 复制代码
arr = null

通过设置arr为null,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了

什么是内存泄漏

变量在脱离上下文环境后或者业务逻辑不需要后,还存在引用关系,即:内存泄漏。

引起内存泄漏的原因

意外的全局变量

由于 js 对未声明变量的处理方式是在全局对象上创建该变量的引用。如果在浏览器中,全局对象就是 window 对象。变量在窗口关闭或重新刷新页面之前都不会被释放,如果未声明的变量缓存大量的数据,就会导致内存泄露。

  • 未声明变量
js 复制代码
function fn() {
  a = 'global variable'
}
fn()
  • 使用 this 创建的变量(this 的指向是 window)。
js 复制代码
function fn() {
  this.a = 'global variable'
}
fn()

解决方法:

  • 避免创建全局变量
  • 使用严格模式,在 JavaScript 文件头部或者函数的顶部加上 use strict

闭包引起的内存泄漏

原因:闭包可以读取函数内部的变量,然后让这些变量始终保存在内存中。如果在使用结束后没有将局部变量清除,就可能导致内存泄露。

JS 复制代码
function outer(x) {
  var count = 0;
  function inner() {
    count += x;
    console.log(count);
  }
  return inner;
}
var add = outer(10);
add();
JS 复制代码
//创建一个防抖函数debounce
function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.真正执行的函数
  const _debounce = function () {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数
      fn()
    }, delay)
  }

  return _debounce
}

解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中。

比如:在循环中的函数表达式,能复用最好放到循环外面。

js 复制代码
// bad
for (var k = 0; k < 10; k++) {
  var t = function (a) {
    // 创建了10次  函数对象。
    console.log(a)
  }
  t(k)
}

// good
function t(a) {
  console.log(a)
}
for (var k = 0; k < 10; k++) {
  t(k)
}
t = null

没有清理的 DOM 元素引用

原因:虽然别的地方删除了,但是对象中还存在对 dom 的引用。

js 复制代码
// 在对象中引用DOM
var elements = {
  btn: document.getElementById('btn'),
}
function doSomeThing() {
  elements.btn.click()
}

function removeBtn() {
  // 将body中的btn移除, 也就是移除 DOM树中的btn
  document.body.removeChild(document.getElementById('btn'))
  // 但是此时全局变量elements还是保留了对btn的引用, btn还是存在于内存中,不能被GC回收
}

解决方法:手动删除,elements.btn = null

被遗忘的定时器或者回调

定时器中有 dom 的引用,即使 dom 删除了,但是定时器还在,所以内存中还是有这个 dom。

js 复制代码
// 定时器
var serverData = loadData()
setInterval(function () {
  var renderer = document.getElementById('renderer')
  if (renderer) {
    renderer.innerHTML = JSON.stringify(serverData)
  }
}, 5000)

// 监听
var btn = document.getElementById('btn')
function onClick(element) {
  element.innerHTMl = "I'm innerHTML"
}
btn.addEventListener('click', onClick)

解决方法:

  • 手动删除定时器和 dom。
  • removeEventListener 移除事件监听

vue 中容易出现内存泄露的几种情况

在 Vue SPA 开发应用,那么就更要当心内存泄漏的问题。因为在 SPA 的设计中,用户使用它时是不需要刷新浏览器的,所以 JavaScript 应用需要自行清理组件来确保垃圾回收以预期的方式生效。因此开发过程中,你需要时刻警惕内存泄漏的问题。

全局变量造成的内存泄露

声明的全局变量在切换页面的时候没有清空

html 复制代码
<template>
  <div id="home">这里是首页</div>
</template>

<script>
  export default {
    mounted() {
      window.test = {
        // 此处在全局window对象中引用了本页面的dom对象
        name: 'home',
        node: document.getElementById('home'),
      }
    },
  }
</script>

解决方案:在页面卸载的时候顺便处理掉该引用。

js 复制代码
destroyed () {
  window.test = null // 页面卸载的时候解除引用
 }

监听在 window/body 等事件没有解绑

特别注意 window.addEventListener 之类的时间监听

js 复制代码
<template>
<div id="home">这里是首页</div>
</template>

<script>
export default {
mounted () {
  window.addEventListener('resize', this.func) // window对象引用了home页面的方法
}
}
</script>

解决方法:在页面销毁的时候,顺便解除引用,释放内存

js 复制代码
mounted () {
  window.addEventListener('resize', this.func)
},
beforeDestroy () {
  window.removeEventListener('resize', this.func)
}

绑在 EventBus 的事件没有解绑

举个例子

js 复制代码
<template>
  <div id="home">这里是首页</div>
</template>

<script>
export default {
  mounted () {
   this.$EventBus.$on('homeTask', res => this.func(res))
  }
}
</script>

解决方法:在页面卸载的时候也可以考虑解除引用

js 复制代码
mounted () {
 this.$EventBus.$on('homeTask', res => this.func(res))
},
destroyed () {
 this.$EventBus.$off('homeTask')
}

Echarts

Echarts常用方法处理:

  1. 实例挂在到vue上
  2. 监听大小变化重新渲染
  3. Echarts自带的on监听方法
js 复制代码
mounted() {
  this.echart = this.$echarts.init(this.$el);
  this.echart.on("finished",this.finished);
  window.addEventListener("resize", this.resize);
},
beforeDestroy () {
  this.echart.dispose();
  this.echart.off("finished");
  window.removeEventListener("resize", this.resize);
  this.echart = null;
}

v-if 指令产生的内存泄露

v-if 绑定到 false 的值,但是实际上 dom 元素在隐藏的时候没有被真实的释放掉。

比如下面的示例中,我们加载了一个带有非常多选项的选择框,然后我们用到了一个显示/隐藏按钮,通过一个 v-if 指令从虚拟 DOM 中添加或移除它。这个示例的问题在于这个 v-if 指令会从 DOM 中移除父级元素,但是我们并没有清除由 Choices.js 新添加的 DOM 片段,从而导致了内存泄漏。

html 复制代码
<div id="app">
  <button v-if="showChoices" @click="hide">Hide</button>
  <button v-if="!showChoices" @click="show">Show</button>
  <div v-if="showChoices">
    <select ref="select"></select>
  </div>
</div>

<script>
  export default {
    data() {
      return {
        showChoices: true,
      }
    },
    mounted() {
      window.select = this.$refs['select']
    },
    methods: {
      show() {
        this.showChoices = true
      },
      hide() {
        this.showChoices = false
      },
    },
  }
</script>

ES6 防止内存泄漏

前面说过,及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。

ES6 考虑到这点,推出了两种新的数据结构: weakset 和 weakmap 。他们对值的引用都是不计入垃圾回收机制的,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存。

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

js 复制代码
const wm = new WeakMap()
const element = document.getElementById('example')
vm.set(element, 'something')
vm.get(element)

上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对 element 的引用就是弱引用,不会被计入垃圾回收机制。

注册监听事件的 listener 对象很适合用 WeakMap 来实现。

js 复制代码
function handler (){
  return new Array(5*1024*1024)
}
// 代码1
$0.addEventListener('click', new Array(5*1024*1024), false)

// 代码2
const listener = new WeakMap()
listener.set($0,handler)
$0.addEventListener('click', listener.get($0), false)

代码 2 比起代码 1 的好处是:由于监听函数是放在 WeakMap 里面,一旦 dom 对象 ele 消失,与它绑定的监听函数 handler 也会自动消失

js 复制代码
// 查看内存
console.log(performance.memory.totalJSHeapSize);

let wm = new WeakMap();
let b = new Object();
wm.set(b, new Array(5*1024*1024));
// 查看内存
console.log(performance.memory.totalJSHeapSize);

b = null
// 先手动回收内存,再查看
console.log(performance.memory.totalJSHeapSize);

会发现当b的指向清理后,WeakMap内set b的内存被清理掉了

相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼5 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT5 小时前
promise & async await总结
前端
Jerry说前后端5 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天5 小时前
A12预装app
linux·服务器·前端