V8中垃圾回收里的巧妙算法和策略

262 阅读6分钟

前言

大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.

我的宗旨就是 万物皆可手写

今天我们来系统性的扒一扒垃圾回收和内存泄漏的相关知识

新手创作不易,有问题欢迎指出和轻喷,谢谢

附上本人的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标记的节点 image.png

2.标记整理

升级版标记清除策略,

  • 普通的标记清除,删除节点后会造成内存空间不连续的问题
  • 标记为垃圾后,进行内存整理,再清除

image.png



image.png

3. 分代式垃圾回收策略

当下V8的垃圾回收优化方案

  • 将堆内存分为两个部分, 新生代(1-8M),老生代
  • 新创建的对象直接进入新生代区域, 大型对象直接放入老生代

image.png

  • 新生代分为两部分(使用区+空闲区) 新生代会快速反复进行如下GC操作

1.标记存活对象,copy进空闲区,

2.清除垃圾对象。

3.使用区和空闲区互换。

  • 如果多次互换后,发现一个对象多次存活,则推入老生代进行保存
  • 对于老生代的对象,就进行普通的标记清除即可(GC间隔比新生代更长)

image.png

为什么需要新老生代?

新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理

而一些大、老、存活时间长的对象作为老生代,使其很少接受检查

新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率

总结:

  • 将内存分为两部分,新生代(快速检查) 老生代(慢速检查)
  • 新生代通过使用区和空闲区交替,检查出能够放入老生代的对象
  • 新老生代可以类比为:计算机的缓存条硬盘内存之间的关系(别杠哈亲,个人理解)

4. 多线程GC优化

并行执行GC

  • 也就是执行GC的时候,多开两个辅助线程提速
  • 这种方式会阻断JS执行,但是由于其是静态的,避免了GC执行时由JS代码导致引用变化,对垃圾回收造成的影响

image.png

增量回收(切片)

  • 将GC过程分成多个片段,插入JS空闲部分执行
  • 熟悉React时间切片的同学可以参考
  • 需要解决JS执行时引用变化对GC的影响问题(可查阅"三色标记法")

image.png

并发回收

  • 直接开启辅助线程进行回收,不会阻碍JS执行
  • 需要解决JS执行时引用变化对GC的影响问题(通过线程通信,加锁等手段)

image.png

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}上无强引用, 可等待垃圾回收

参考文章(推荐好文)

「硬核JS」你的程序中可能存在内存泄漏 - 掘金 (juejin.cn)

「硬核JS」你真的了解垃圾回收机制吗 - 掘金 (juejin.cn)