前言
大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.
我的宗旨就是 万物皆可手写
今天我们来系统性的扒一扒垃圾回收和内存泄漏的相关知识
新手创作不易,有问题欢迎指出和轻喷,谢谢
附上本人的git仓库 github.com/lzy19926, 支持的大佬们可以进来给本人的一些学习项目点个star嘛(是的我就是厚颜无耻要star)
内存泄漏
- 引擎中有垃圾回收机制,它主要针对一些程序中不再使用的对象,对其清理回收释放掉内存。
- 如果创建了对象,而没有被回收掉,随着程序的多次运行,内存会越来越满,导致卡顿
- V8垃圾回收机制: 标记清除 引用计数 手动释放(obj=null)
造成内存泄漏常见的原因
- 不正当的闭包
- 隐式全局变量
// 没有声明从而制造了隐式全局变量test1
test1 = new Array(1000).fill('isboyjc1')
// 函数内部this指向window,制造了隐式全局变量test2
this.test2 = new Array(1000).fill('isboyjc2') } fn()
- 游离DOM引用
//-----------------
<ul id="ul">
<li id="li3"></li>
</ul>
//-----------------
let ul = document.querySelector('#ul')
let li = document.querySelector('#li3')
// 由于ul变量存在,整个ul节点及其子元素都不能GC
root.removeChild(ul)
// 虽置空了ul变量,但由于li变量引用ul的子节点,所以ul元素依然不能被GC
ul = null
- 遗忘的定时器,事件监听器,requestAnimationFrame
// 由于timeout,interval,eventListener等都是直接挂载到window上的,视为全局变量,使用后需要清除
const interval = setInterval(() => {...}, 1000)
clearInterval(interval)
const timeout = setTimeout(() => {...}, 1000)
clearTimeout(timeout)
window.addEventListener("click", doSomething)
window.removeEventListener("click", doSomething)
cosnt raf = requestAnimationFrame(()=>{},1000)
cancelAnimationFrame(raf)
- 遗忘的Map、Set对象
使用 Map 或 Set 存储对象时,同 Object 一致都是强引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收。(详见下文)
推荐一篇非常好的内存泄漏文章: 「硬核JS」你的程序中可能存在内存泄漏 - 掘金 (juejin.cn)
垃圾回收
1.标记清除
最常见的垃圾清除策略,
- 浏览器堆内存中存放了多个需要垃圾回收的root节点 (比如window对象,Dom树等等)
- 所有初始节点都标记为0, 遍历树,不需要清理的节点设置为1
- 清理0标记的节点
2.标记整理
升级版标记清除策略,
- 普通的标记清除,删除节点后会造成内存空间不连续的问题
- 标记为垃圾后,进行内存整理,再清除
3. 分代式垃圾回收策略
当下V8的垃圾回收优化方案
- 将堆内存分为两个部分, 新生代(1-8M),老生代
- 新创建的对象直接进入新生代区域, 大型对象直接放入老生代
- 新生代分为两部分(使用区+空闲区) 新生代会快速反复进行如下GC操作
1.标记存活对象,copy进空闲区,
2.清除垃圾对象。
3.使用区和空闲区互换。
- 如果多次互换后,发现一个对象多次存活,则推入老生代进行保存
- 对于老生代的对象,就进行普通的标记清除即可(GC间隔比新生代更长)
为什么需要新老生代?
将新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理
而一些大、老、存活时间长的对象作为老生代,使其很少接受检查
新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率
总结:
- 将内存分为两部分,新生代(快速检查) 老生代(慢速检查)
- 新生代通过使用区和空闲区交替,检查出能够放入老生代的对象
- 新老生代可以类比为:计算机的
缓存条和硬盘内存之间的关系(别杠哈亲,个人理解)
4. 多线程GC优化
并行执行GC
- 也就是执行GC的时候,多开两个辅助线程提速
- 这种方式会阻断JS执行,但是由于其是静态的,避免了GC执行时由JS代码导致引用变化,对垃圾回收造成的影响。
增量回收(切片)
- 将GC过程分成多个片段,插入JS空闲部分执行
- 熟悉React时间切片的同学可以参考
- 需要解决JS执行时引用变化对GC的影响问题(可查阅"三色标记法")
并发回收
- 直接开启辅助线程进行回收,不会阻碍JS执行
- 需要解决JS执行时引用变化对GC的影响问题(通过线程通信,加锁等手段)
weakMap和weakSet
- 首先你得知道什么是map,set数据结构
我们来看一下weakMap和Map的区别,weakMap是通过弱引用与其引用对象链接的,而Map则是强引用
// weakMap --->表示弱引用
{
[key]--->value,
[key2]--->value2
}
// Map ===>表示强引用
{
[key]===>value,
[key2]===>value2
}
weakSet同理
// weakSet --->表示弱引用
{
"0"--->value,
"1"--->value2
}
// Set ===>表示强引用
{
"0"===>value,
"1"===>value2
}
强引用和弱引用
垃圾回收机制中,引用数为0的内存空间,会被回收。(而这里的引用数只计算强引用数)
如果一个对象身上只有弱引用,则也会被垃圾回收。
啥也不说,上代码。
- 强引用
let obj = {id: 1} // 开辟内存空间,创建普通对象,强引用到obj上
obj = null // 删除引用,此时对象身上的引用数为0, 可等待CG的引用计数策略删除
- 强引用2
let obj = {id: 1} //开辟内存空间
let set = new Set([obj]) // 保存obj到set中
// 此时 obj和set都于对象{id:1}有强引用关系
// 删除一个引用 此时set依然强引用了{id:1} (不会被垃圾回收)
obj = null
// 依然可以通过set[0]访问到{id:1}
console.log(set)
- 弱引用
let obj = {id: 1} //开辟内存空间
let ws = new WeakSet([obj]) // 创建weakSet,与对象建立弱引用
// 此时obj与对象是强引用关系 ws与对象是弱引用关系
//删除obj引用,此时对象{id: 1}上只有一个弱引用
obj = null
// 因为{id:1}上无强引用, 可等待垃圾回收