文章目录

  • 内存溢出
  • js的垃圾回收机制
  • 什么是这里所谓的垃圾和其清楚方法
  • 引用计数垃圾收集
  • 标记清除法
  • 内存泄露
  • 定义
  • 八大内存泄露的情景及解决办法
  • 意外的全局变量
  • 计时器或回调函数没被清除
  • 被遗忘的时间监听器
  • 闭包
  • 被遗忘的 ES6 Set 和Map 成员
  • 被遗忘的订阅发布事件监听器
  • 脱离DOM的引用


内存溢出

定义:当程序运行需要的内存超过了剩余的内存是,就会爬出内存溢出的错误,可以理解为一种程序运行错误

const obj = {}
for(let i=0;i<10000;i++){
  obj.i = new Array(1000000000)
}

如果你将上面代码运行一下,在打开你的任务管理器看看内存的情况,我想你应该知道是啥回事了

js的垃圾回收机制

注意看标题,标题是js的垃圾回收机制,比如c语言和c++等语言是有专门的内存管理接口的,比如free()函数来释放内存,但是javascript不同,js是在穿件变量的过程中自动进行了内存分配,在不使用他们之后会自动释放内存,此过程被称之为垃圾回收

什么是这里所谓的垃圾和其清楚方法

这里所谓的垃圾有如下两种:

垃圾种类

所谓的清除方法

没有被引用的对象,(还有一种特别特殊的情况,几个对象的引用形成了一个环)

引用计数垃圾收集

当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,而标记为“离开环境”的变量则可以被回收

标记清除法

引用计数垃圾收集

看代码和注释:

// “对象 { x:1}”分配给 a 变量,引用了此对象
let a = {
    x:1
}

//  b 现在也引用了{ x:1}对象
let b = a

// a没有指向这个对象了,这时对象{ x:1}还是被b引用了
a = 0

怎么让对象被回收,就是替换这个引用,让b=null最好,b=0也都可以

标记清除法

上面所说的变量进出环境,其实就是作用域

//全局环境下
// b 被标记进入环境
var b = 2;
function test() {
  var a = 1;
  // 函数执行时,a 被标记进入环境
  return a + b;
}
// 函数执行结束,a 被标记离开环境,被回收
// 但是 b 就没有被标记离开环境
test();

内存泄露

定义

知道了什么是js的垃圾回收机制后就更能很好的理解什么是内存泄露了,其实简单来说:内存泄露就是没有被垃圾回收机制回收的垃圾占用了内存没有被释放。

内存泄露是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。

八大内存泄露的情景及解决办法

意外的全局变量

全局作用域的变量只会在页面关闭才会销毁:因为这里在函数中没声明的变量会是全局变量,且在全局环境下没有被标记,使用后不能被标记清楚法清楚,所以只能在页面关闭时被清除

function test(b){
    a = 0 // 这里的变量a相当于window.a
    return  a+b
}

计时器或回调函数没被清除

浏览器一般会认为setInterval在没用clearInterval方法清除前占用的内存是必须内存,所以需要

timer = setInterval(function() {
.........
}, 1000);

只有在不使用时(一般是这个组件销毁的时候),使用clearInterval方法才能回收

clearInterval(timer)

被遗忘的时间监听器

window.addEventListener('resize', () => {
      // 这里做一些操作
    })

要移除才能回收

window.removeEventListener('resize', this.resizeEventCallback)

闭包

闭包中的变量是不具有垃圾回收机制的。这是一个优点,同时也是一个缺点,缺点就是造成内存泄露

function fn1(){
    var a = 1
    function fn2(){
        console.log(a++);
    }
    return fn2()
}

var f = fn1
f()

上面的a变量就是没被回收的
所以执行下面的才会释放内存

f = null

被遗忘的 ES6 Set 和Map 成员

下面如果没有map.delete(value);是泄露的

let map = new Set();
let value = { test: 22};
map.add(value);

map.delete(value);
value = null;

map的方法和set差不多,也是如果最后没使用delete就泄露了

所以为了解决这个麻烦,最后产生了 WeakSet,WeakMap

被遗忘的订阅发布事件监听器

这个知识点是针对vue的:涉及到具有关系的组件之间的传值。

<template>
  <div @click="onClick"></div>
</template>

<script>
import customEvent from 'event'

export default {
  methods: {
    onClick() {
      customEvent.emit('test', { type: 'click' })
    },
  },
  mounted() {
    customEvent.on('test', data => {
      // 一些逻辑
      console.log(data)
    })
  },
}
</script>

上面的组件销毁的时候,自定义 test 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:

<template>
  <div @click="onClick"></div>
</template>

<script>
import customEvent from 'event'

export default {
  methods: {
    onClick() {
      customEvent.emit('test', { type: 'click' })
    },
  },
  mounted() {
    customEvent.on('test', data => {
      // 一些逻辑
      console.log(data)
    })
  },
  beforeDestroy() {
    customEvent.off('test')
  },
}
</script>

脱离DOM的引用

有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
}
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}

这时需要使这个button 为null 才会被回收