JavaScript内存泄漏与垃圾回收机制

101 阅读5分钟

1.内存泄漏

1-1.什么是内存泄漏:

不再用到的内存,没有被及时释放,就叫做内存泄漏

1-2.造成内存泄漏的原因:

1-2-1. 闭包使用不合理

function fn2(){
  let colors = ['res', 'yellow', 'blue']
  return function(){
    console.log(colors)
    return colors
  }
}
let fn2Child = fn2()
fn2Child()

当函数fn2执行完成时,变量colors没有被回收,造成了内存泄漏。修改方法:

function fn2(){
  let colors = ['res', 'yellow', 'blue']
  return function(){
    console.log(colors)
    return colors
  }
}
let fn2Child = fn2()
fn2Child()
// 函数fn2Child执行完成后,将其置为null,此时colors变量就可以被垃圾回收机制回收回收
fn2Child = null

1-2-2.隐式创建的全局变量

function fn2(){
  // colors没有使用var/let/const声明,会将colors创建为全局变量(window的属性),即使函数fn2执行完成后,变量依然不会被垃圾回收机制回收。
  colors = ['res', 'yellow', 'blue']
  // 此处this指向全局对象(window),与colors同理不会被垃圾回收机制回收。
  this.names = ['zhangsan', 'lisi', 'wangwu']
}
fn2()

在使用全局变量后,要注意将其置为null,否则其占用的内存空间无法被释放,造成内存泄漏。

1-2-3. 未清理的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')
  let li3 = document.querySelector('#li3')
  
  // 由于ul变量存在,整个ul及其子元素都不能GC
  root.removeChild(ul)
  
  // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
  ul = null
  
  // 已无变量引用,此时可以GC
  li3 = null
</script>

1-2-4.遗忘的定时器

// 获取数据
let names = ['zhangsan', 'lisi','wangwu']
let count = 0
setInterval(() => {
  if(count < 3) {
    console.log(names[count])
    count ++
  } else {
    count = 0  
  }
  
}, 1000)

在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。如果没有被 clear 掉的话,就会造成内存泄漏(回调函数及其依赖的变量都无法被回收)。

2.垃圾回收机制

2-1.不同语言的垃圾回收策略

垃圾数据回收分为手动回收自动回收两种策略。

手动回收:如 C/C++ 就是使用手动回收策略,何时分配内存、何时销毁内存都是由代码控制的。

自动回收:如 JavaScriptJavaPython 等语言,产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。

2-2.调用栈中的数据是如何回收的

ESP:记录当前执行状态的指针,指向调用栈中函数的执行上下文,表示当前正在执行的函数。

调用栈用于存储函数执行上下文,当一个函数执行完成时,ESP会向下移动,指向下一层的执行上下文。这个下移操作就是销毁已经执行完成的函数的执行上下文的过程。

2-3.堆中的数据是如何回收的

2-3-1.代际假说

  • 大部分对象在内存中存在的时间都很短。
  • 不死的对象,会活得更久。

大多数的动态语言都符合上述规律。

2-3-2.分代收集

JavaScript将对内存分为两块区域:新生代区域、老生代区域。

新生代存放生存时间短的数据;老生代存放生存时间长的数据。

副垃圾回收器:负责新生代中的垃圾回收。

主垃圾回收器:负责老生代中的垃圾回收。

2-3-3.垃圾回收的工作流程

无论是副垃圾回收器还是主垃圾回收器,进行垃圾回收过程主要有3个步骤:标记-清除-整理

标记:将堆内存中的数据标记为活动对象非活动对象

清除:标记完成后一次性回收内存中被标记为可回收的对象。

整理:可回收对象被清除后,会产生一些内存碎片,当内存碎片过多且需要分配一大块内存出来时,会出现内存不足的现象。所以需要进行内存整理。(注意:有的垃圾回收过程中不会产生内存碎片,也就不需要整理。副垃圾回收器就不需要。)

2-3-4.副垃圾回收器

副垃圾回收器负责新生区的垃圾回收。

Scavenge算法:将新生区分为对象区和空闲区,当新的数据需要内存时,会在对象区内分配内存,当对象区内存快满时,对对象区进行垃圾回收处理,然后将对象区内仍然存在的数据复制到空闲区(按顺序排好,所以复制过程就消除了内存碎片),最后将对象区和空闲区角色互换。

新生区内存较小:运行Scavenge算法需要进行数据的复制,内存太大会导致复制的数据量太大导致执行效率变低。

对象晋升策略:当经过两次垃圾回收操作后让然存在的数据,这些数据会被存放到老生区中。

2-3-5.主垃圾回收器

老生区中对象特点:数据较大、存活时间长。

标记-清除算法:从一组跟元素开始,递归遍历这组跟元素,遍历到的数据标记为活动对象,没有遍历到的数据标记为垃圾数据。标记完成后进行垃圾清除操作。最后再进行整理操作消除内存碎片。

2-4.全停顿

全停顿:主线程在运行JavaScript代码过程中,当要进行垃圾回收操作时,会暂停JavaScript代码的运行,等垃圾回收操作完成后再运行JavaScript代码。

全停顿影响:当垃圾回收操作时间较长时,会使页面出现明显卡顿。

增量标记算法:v8将垃圾回收标记过程拆分成一个个子标记过程,让子标记过程和JavaScript代码的运行交替进行,直到标记完成。

增量标记算法优点:降低垃圾回收过程中页面产生的卡顿感。