开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天
垃圾回收和内存泄漏是因果关系,垃圾没被完全回收就会造成内存泄漏
一、垃圾怎么产生
编码过程中,创建一个变量,引擎会自动为其分配内存,根据变量的数据类型给其分配栈内存或堆内存。引用类型在栈中存储地址,指向堆中的实体。
//1.栈中创建变量a指针指向堆中实体{name:aaa}
var a = {name:aaa}
//2.a指向堆中另一个实体[1]
a=[1]
//3.这时{name:aaa}变成无用的垃圾,若不及时清理,类似的垃圾越来越多,会造成系统崩溃。所以有了垃圾回收机制。
二、垃圾回收机制
定期找出不用的内存(变量)并清除。找的过程用到两种常见的算法:标记清除算法、引用计数算法。
2.1标记清除算法
顾名思义先标记后清除
- 回收机制运行时给所有变量都打上标记,比如0,假设都是垃圾
- 从根对象遍历,不是垃圾节点标记1,变量进入执行环境,标记“进入环境”
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间,剩下都是标记为1
- 将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