垃圾回收与内存泄漏

124 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天

垃圾回收和内存泄漏是因果关系,垃圾没被完全回收就会造成内存泄漏

一、垃圾怎么产生

编码过程中,创建一个变量,引擎会自动为其分配内存,根据变量的数据类型给其分配栈内存或堆内存。引用类型在栈中存储地址,指向堆中的实体。

//1.栈中创建变量a指针指向堆中实体{name:aaa}
var a = {name:aaa}
//2.a指向堆中另一个实体[1]
a=[1]
//3.这时{name:aaa}变成无用的垃圾,若不及时清理,类似的垃圾越来越多,会造成系统崩溃。所以有了垃圾回收机制。

二、垃圾回收机制

定期找出不用的内存(变量)并清除。找的过程用到两种常见的算法:标记清除算法、引用计数算法。

2.1标记清除算法

顾名思义先标记后清除

  1. 回收机制运行时给所有变量都打上标记,比如0,假设都是垃圾
  2. 从根对象遍历,不是垃圾节点标记1,变量进入执行环境,标记“进入环境”
  3. 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间,剩下都是标记为1
  4. 将1改为0,进入下一轮垃圾回收

缺点:清除后剩余内存位置不变,碎片化内存,新建分配对象需要遍历剩余内存找到大于等于size的内存才能分配。从而有了标记整理,将空内存整理到内存一端,清理边界内存。

2.2引用计数算法

判断是否有其他对象引用它

  • 引用表

引擎有一张"引用表",保存内存里面所有的资源的引用次数。如果引用次数是0,表示这个值不再用到,因此可以将这块内存释放。若不再用到该变量,引用次数也不为0,说明发生内存泄漏。

//变量arr地址指向[1,2,3],[1,2,3]这块内存引用次数为1
var arr = [1,2,3]
//[1,2,3]这块内存不再被arr引用,引用次数减1,可以被回收
arr = [1,2]
  • 循环引用

A引用B,B引用A。引用次数永远不为0,需要将其手动设为null

function fn(){
let A= new Object()
let B=new Object()
A.b = B 
B.a = A
} 

总结

标记清除隔一段时间遍历清除,引用计数当引用次数为0时立即清除,引用计数需要计数器且不知道引用次数上限,会有循环引用问题。标记清除不会有循环引用问题。常用的是标记清除算法,而V8中垃圾回收做了优化。

2.3 V8中的GC

V8在原来的标记清除算法上做了优化,原来的垃圾回收每次都会遍历内存上所有对象,而V8将这些对象分为新生代和老生代,新生代代表新 小 存活时间短,老生代代表大,老,存活时间长。两种类型相比,新生代更适合频繁清理。V8堆内存分为新生代(1-8M)和老生代区域,两个区域采用不同的垃圾回收策略。老生代的对象则是经历过新生代垃圾回收后还存活的对象。

新生代

通过Scavenge算法进行回收,具体实现时采用复制式方法即Cheney算法

将新生代区域分为使用区和空闲区,新加入的对象放入使用区,当使用区快被写满时,执行垃圾清理操作,垃圾回收时,对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并排序,随后在清理掉非活动对象。调换使用区和空闲区。若某对象多次复制后依然存活就放入老生代区域,或者某对象复制到空闲区后,空闲区占用空间超过25%则直接放入老生代空间。因为后续对调使用区和空闲区,影响后续内存分配。

老生代

老生代流程就是原来的标记清除算法,由于内存碎片化,所以V8在原来的基础上使用的标记整理算法。

2011年,V8对老生代标记进行优化,全停顿标记切换为增量标记

回收方式

  • 并行回收
  • 增量标记
  • 惰性清理
  • 并发回收

三、内存泄漏场景

3.1 不正当的闭包

3.2隐式全局变量

用全局变量需要置空

//1.未声明的变量,页面关闭后才会清除
bar = 3
//2.this使用不当
function foo() { 
this.variable = "global"; // foo 调用自己,this 指向了全局对象(window)
} 
foo();

3.3 计时器、回调函数

定时器没有被清除,其内部的回调函数会造成内存泄漏

3.4 事件监听器、监听者模式

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

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

<script>
export default {
  created() {
    window.addEventListener("resize", this.doSomething)
    eventBus.on("test", this.doSomething)
  },
  beforeDestroy(){
    window.removeEventListener("resize", this.doSomething)
    eventBus.off("test", this.doSomething)
  },
  methods: {
    doSomething() {
      // do something
    }
  }
}
</script>

3.5 map set对象

使用弱引用

let obj = {id: 1} 
let weakSet = new WeakSet([obj]) 
let weakMap = new WeakMap([[obj, 'hahaha']]) // 重写obj引用 
obj = null // {id: 1} 将在下一次 GC 中从内存中删除

3.6 未清理的DOM元素引用

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 

3.7 console.log