复盘:js中的垃圾回收机制和内存泄漏

251 阅读5分钟

js内存管理机制

  • js在创建变量(对象、字符串等)时自动进行了分配内存,并且在不使用它们时自动释放。释放的过程称为垃圾回收
  • 自动是混乱的根源,并让js开发者错误的感觉它们可以不关心内存管理。
  • js内存管理机制和内存的生命周期是一一对应的,首先需要分配内存,然后使用内存,最后释放内存
  • js语言不需要程序员手动分配内存,绝大多数情况下也不需要手动释放内存,对js程序员来说通常就是使用内存(即使用变量、函数、对象等)

内存分配

// 给Boolean类型变量分配内存
let flag = true;
// 给字符串类型变量分配内存
let str = 'hello';
// 给数组及其包含的值分配内存
let arr = [2, null, 'aa'];
// 给函数分配内存
function fn (a) {
  return a;
}

内存使用

  • 使用值的过程实际上是对分配内存进行读取与写入的操作。
  • 读取与写入可能是:写入一个变量或者一个对象的属性值,甚至传递函数的参数。
// 写入内存
str = 'byebye';
// 读取str 和 fn 的内存,写入fn参数内存
fn(str);

内存回收

  • 垃圾内存回收GC(Garbage Collection)
  • 内存泄漏一般都是发生在这一步,js的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况,如果存在这种情况,需要手动清理内存。

js垃圾回收的两种方式

引用计数垃圾收集

  • 把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。
  • 如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
// 将对象字面量 分配给 a变量
let a = {
  a: 1,
  b: 2
}
// b 引用 a 变量
let b = a;
// a赋值为1,将对象字面量的引用替换给b
a = 1;
  • 当前执行环境中,对象字面量的内存还没有被回收,需要手动释放内存。
// 或者b = 1,反正替换“对象字面量”,让其不再被引用即可
b = null;

标记清除法

  • 当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,而标记为“离开环境”的变量则是可以被回收。
  • 环境可以理解为作用域,但是全局作用域的变量只会在页面关闭才会销毁。
// 假设这里是:全局作用域
// b 被标记:进入环境
var b = 100;
function add () {
  var a = 1;
  // 函数执行时,a被标记进入环境
  return a + b;
}
add();
// 函数执行完成后,a被标记离开环境,被回收
// 但是b就没有被标记离开环境,因为b是全局变量,只有页面关闭才会被销毁。

js内存泄露场景

  • 下面例子:是在执行环境中,没离开当前执行环境,还触发标记清除法。

意外的全局变量

// 在全局作用域中定义
function count(number) {
  // basicCount 相当于 window.basicCount = 2;
  basicCount = 2;
  return basicCount + number;
}
  • 现在已经能够避免这种错误,eslint会直接报错。

被遗忘的计时器

  • 无用的计时器记得删除。
<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
      // 获取一些数据
    },
  },
  mounted() {
    setInterval(function() {
      // 轮询获取数据
      this.refresh()
    }, 2000)
  },
}
</script>
  • 上面的组件销毁的时候,setInterval还是在运行的,里面涉及到的内存是没法回收的(浏览器会认为这是必须 的内存,不是垃圾内存),需要在组件销毁的时候清除计时器,如下:
<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
      // 获取一些数据
    },
  },
  mounted() {
    this.refreshInterval = setInterval(function() {
      // 轮询获取数据
      this.refresh()
    }, 2000)
  },
  // 在组件销毁之前,清除定时器
  beforeDestroy() {
    clearInterval(this.refreshInterval)
  },
}
</script>

被遗忘的事件监听器

  • 无用的事件监听器记得清理。
<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    window.addEventListener('resize', () => {
      // 这里做一些操作
    })
  },
}
</script>
  • 上面的组件销毁的时候,resize事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:
<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    this.resizeEventCallback = () => {
      // 这里做一些操作
    }
    window.addEventListener('resize', this.resizeEventCallback)
  },
  // 在组件销毁之前移除 监听事件
  beforeDestroy() {
    window.removeEventListener('resize', this.resizeEventCallback)
  },
}
</script>

被遗忘的ES6 Set\Map成员

  • setmap出错和处理方式相同
let map = new Set();
let value = { test: 22};
map.add(value);

// 只将value变成了零引用,但是set集合中还存在
value= null;
  • 需改如下:
let map = new Set();
let value = { test: 22};
map.add(value);

map.delete(value);
value = null;

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

  • 正确写法
<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>

被遗忘的闭包

  • 存在内存泄漏,如下:
function closure() {
  const name = 'xianshannan'
  return () => {
    return name
      .split('')
      .reverse()
      .join('')
  }
}
// 未被调用执行,存在内存泄漏
const reverseName = closure()
  • 在当前执行环境未结束的情况下,严格来说,这样是有内存泄漏的,name 变量是被 closure 返回的函数调用了,但是返回的函数没被使用,这个场景下 name 就属于垃圾内存。name 不是必须的,但是还是占用了内存,也不可被回收。

脱离DOM的引用

  • 每个页面上的 DOM 都是占用内存的,假设有一个页面 A 元素,我们获取到了 A 元素 DOM 对象,然后赋值到了一个变量(内存指向是一样的),然后移除了页面的 A 元素,如果这个变量由于其他原因没有被回收,那么就存在内存泄漏,如下:
class Test {
  constructor() {
    this.elements = {
      button: document.querySelector('#button'),
      div: document.querySelector('#div'),
      span: document.querySelector('#span'),
    }
  }
  removeButton() {
    document.body.removeChild(this.elements.button)
    // this.elements.button = null
  }
}

const a = new Test()
a.removeButton()
  • 上面的例子 button 元素 虽然在页面上移除了,但是内存指向换为了 this.elements.button,内存占用还是存在的。所以上面的代码还需要这样写: this.elements.button = null,手动释放这个内存。

附录