垃圾回收机制 & 内存泄露

370 阅读4分钟

(一)垃圾回收机制

1. 垃圾回收机制都有哪些策略?⭐⭐⭐⭐⭐

(1)标记清除法

  • 当变量进入上下文(比如在函数内部声明一个变量时),这个变量会被加上存在于上下文中的标记
    当变量离开上下文时,也会被加上离开上下文的标记
  • 垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后再访问这些对象并标记它们的引用…
    如此递进结束后若发现有没有标记的(不可达的)进行删除
  • 进入执行环境的不能进行删除

(2)引用计数法(被标记清除取代)

其思路是对每个值都记录它被引用的次数

  • 当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,
    当该值赋值给另一个变量的时候,该计数+1,
    当该值被其他值取代的时候,该计数-1,
    当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
  • 缺点: 当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代

(二)内存泄漏

1. 什么是内存泄漏⭐⭐⭐⭐⭐

引擎中有垃圾回收机制,它主要针对一些程序中不再使用的对象(基本类型会自动被回收),对其清理回收释放掉内存。

内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用

2. 为什么会导致的内存泄漏⭐⭐⭐⭐⭐

  • 简单一句话,该被清除的变量没被清除;
  • 内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃

3. 导致内存泄漏的情况

(1) 不正当的使用闭包

  • 返回函数引用了所在函数作用域的变量
  • 减少闭包的使用,在函数调用后,应及时把外部的引用关系置空
function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test 
    // `return` 的函数中存在函数 `fn2` 中的 `test` 变量的引用,
    // 所以 `test` 并不会被回收,也就造成了内存泄漏
  }
}
let fn2Child = fn2()
fn2Child()
fn2Child = null // 及时把外部的引用关系置空

(2) 隐式全局变量

  • 对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以全局变量通常不会被回收
  • 在使用完将其置为 null 即可
function fn(){
  // 没有声明从而制造了隐式全局变量test1
  test1 = new Array(1000).fill('isboyjc1')
  // 函数内部this指向window,制造了隐式全局变量test2
  this.test2 = new Array(1000).fill('isboyjc2')
}
fn()

(3)游离DOM引用

  • 当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏

  • 考虑到性能或代码简洁方面,我们代码中进行 DOM 时会使用变量缓存 DOM 节点的引用,但移除节点的时候,我们应该同步释放缓存的引用,否则游离的子树无法释放。

<div id="root">
  <ul id="ul">
    <li></li>
    <li></li>
    <li id="li3"></li>
    <li></li>
  </ul>
</div>
<script>
  let root = document.querySelector('#root')
  let ul = document.querySelector('#ul') // ul变量引用ul的子节点
  let li3 = document.querySelector('#li3') // li3变量引用ul的子节点
  
  root.removeChild(ul)// 由于ul变量存在,整个ul及其子元素都不能GC
  ul = null // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
  li3 = null // 已无变量引用,此时可以GC
</script>

(4)遗忘的定时器

let func = getData()// 获取数据
setInterval(() => {
  const node = document.getElementById('Node')
	if(node) {
    node.innerHTML = JSON.stringify(func))
	}
}, 1000)
  • 上例中,在 setInterval 没有结束前,回调函数里的变量以及回调函数func本身都无法被回收。
  • 只有调用了 clearInterval才算结束。如果没有被 clear 掉的话,就会造成内存泄漏。不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。
  • 下列方法也会存在同样的问题,当不需要使用时,应及时调用对应的方法来取消使用
    标题
    setIntervalclearInterval
    setTiemoutclearTimeout
    requestAnimationFramecancelAnimationFrame

(5)遗忘的事件监听器

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

(6)未清理的Console输出

(7)遗忘的Map、Set对象

(8)遗忘的监听者模式

(三)内存泄漏排查、定位与修复