常见的内存泄漏场景

180 阅读5分钟

内存泄漏Memory Leak是指程序中已动态分配的堆内存由于疏忽或错误等原因程序未释放或无法释放, 造成系统内存的浪费, 导致程序运行速度减慢甚至系统崩溃等严重后果. 内存泄漏并非指内存在物理上的消失, 而是应用程序分配某段内存后, 由于设计错误, 导致在释放该段内存之前就失去了对该段内存的控制, 从而造成了内存的浪费. 对于内存泄漏的检测, Chrome提供了性能分析工具Performance, 可以比较方便的地查看内存的占用情况等

意外的全局变量

JavaScript中并未严格定义对未声明变量的处理方式, 即使在局部函数作用域中依旧能够定义全局变量, 这种意外的全局变量可能会存储大量数据, 且由于其是能够通过全局对象例如window能够访问到的, 所以进行内存回收时不认为其时需要回收的内存而一直存在, 只有在窗口关闭或者刷新页面时才能够被释放, 造成意外的内存泄漏, 在JavaScript的严格模式下此种意外的全局变量定义方式会抛出异常, 另外同样可以使用eslint进行此种状态的预检查. 事实上定义全局变量并不是一个好习惯, 如果必须使用全局变量存储大量数据时, 确保用完以后把它设置为null或者重新定义, 与全局变量相关的增加内存消耗的一个主因是缓存, 缓存数据是为了重用, 缓存必须有一个大小上限才有用, 高内存消耗导致缓存突破上限, 因为缓存内容无法被回收

function funct() {
  name = "name";
}
funct();
console.log(window.name);					// name
delete window.name;								// 不手动删除则在不关闭或刷新窗口的情况下一直存在

被遗忘的计时器

计时器setInterval必须及时清理, 否则可能由于其中引用的变量或者函数都被认为是需要的而不会进行回收, 如果内部引用的变量存储了大量数据, 可能会引起页面占用内存过高, 这样就会造成意外的内存泄漏

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

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

脱离DOM的引用

有时保存DOM节点内部数据结构很有用, 例如需要快速更新表格的几行内容, 把每一行DOM存成字典或者数组很有意义. 此时同样的DOM元素存在两个引用: 一个在DOM树中, 另一个在字典中. 将来如果决定删除这些行时, 需要把两个引用都清除. 此外还要考虑DOM树内部或子节点的引用问题, 假如你的JavaScript代码中保存了表格某一个<td>的引用, 将来决定删除整个表格时时候, 直觉认为GC会回收除了已保存的<td>以外的其他节点, 实际情况并非如此, 此<td>是表格的子节点, 子元素与父元素是引用关系, 由于代码保留了<td>的引用, 导致整个表格仍待在内存中, 所以在保存DOM元素引用的时候, 要小心谨慎

let elements = {
  button: document.getElementById("button");
  image: document.getElementById("image");
  text: document.getElementById("text");
};

function doStuff() {
  elements.image.src = "https://www.example.com/1.jpg";
  elements.button.click();
  console.log(elements.text.innerHTML);
}

function removeButton() {
  // 按钮是body的后代元素
  document.body.removeChild(elements.button);
  // 清除对于这个对象的引用
  elements.button = null;			
}

闭包

闭包是JavaScript开发的一个关键方面, 闭包可以让你从内部函数访问外部函数作用域, 简单来说可以认为是可以从一个函数作用域访问另一个函数作用域而非必要在函数作用域中实现作用域链结构. 由于闭包会携带包含它的函数的作用域, 因此会比其他函数占用更多的内存, 过度使用闭包可能会导致内存占用过多, 在不再需要的闭包使用结束后需要手动将其清除

function debounce(wait, funct, ...args) {
  let timer = null;
  return () => {
    clearTimeout(timer);
    timer = setTimeout(() => funct(..args), wait);
  }
}
window.onscroll = debounce(300, (a) => console.log(a), 1);

被遗忘的监听者模式

当实现了监听者模式并在组件内挂载相关的事件处理函数, 而在组件销毁时不主动将其清除时, 其中引用的变量或者函数都被认为是需要的而不会进行回收, 如果内部引用的变量存储了大量数据, 可能会引起页面占用内存过高, 这样就造成了意外的内存泄漏

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

<script>
export default {
  created: function() {
    created: function() {
      global.eventBus.on("test", this.doSomething);
    },
    beforeDestroy: function() {
      global.eventBus.off("test", this.doSomething);
    },
    methods: {
      doSomething: function() {
        // do something
      }
    }
  }
}
</script>

被遗忘的Set

当使用Set存储对象时, 类似于脱离DOM当引用, 如果不将其主动清除引用, 其同样会造成内存不自动进行回收, 如果需要使用Set引用对象, 可以采用WeakSet, WeakSet对象允许存储对象弱引用的唯一值, WeakSet对象中的值同样不会重复, 且只能保存对象的弱引用, 且由于是对于对象的弱引用, 其不会干扰JavaScript的垃圾回收

let elements = new Set();
let btn = document.getElementById("button");
elements.add(btn);
function doStuff() {
  btn.click();
}

function removeButton() {
  document.body.removeChild(btn);				// 按钮是body的后代元素
  elements.delete(btn);									// 清除Set中对于这个对象的引用
  btn = null;														// 清除引用
}