背景
JS会在创建变量时自动分配内存,在不使用的时候会自动周期性的释放内存,释放的过程就叫 “垃圾回收”。
垃圾如何产生
我们在写代码时会创建一个基本类型、对象、函数……这些都是需要占用内存的,但是我们并不关注这些,因为这是引擎为我们分配的,我们不需要显式手动的去分配内存。但如果内存分配之后又没有及时的回收,就会导致内存泄漏,也就是内存垃圾的产生。
程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。
垃圾回收策略
垃圾回收策略主要分两种,一是引用计数
,二是分代回收
(V8垃圾回收策略主要基于 分代式垃圾回收机制)
引用计数(Reference Counting):这个策略是比较早的一种垃圾回收算法,它把 对象是否不再需要,简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
但引用计数有个缺点就是 循环引用不能为0
,举个例子:
const a = 1; b = a; c = a;
变量被多次引用,就会导致引用计数失效,也就是说循环引用不可能为0。
两个都互相引用了,引用计数不为0,所以两个变量都无法回收。
如果频繁的调用改函数,则会造成很严重的内存泄漏。
分代回收:V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。
新生代
Scavenge 算法
存放生命周期短的对象from(内存区域也就是被使用的区域)/to,扫描 ⇒ 存活对象copy到to, 16M
新生代使用
Scavenge
算法实现GC
,它将 新生代分为两个半区域,分别成为from空间和to空间
一次垃圾回收 ♻️ 分为两步:- 将 from 空间的活对象(从 roor 可以访问到)复制到 to
- 切换 from、to 角色
如何回收新生代对象?
检查From
空间内的存活对象,若对象存活,检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从From
空间复制到To
空间。若对象不存活,则释放不存活对象的空间。完成复制后,将From
空间与To
空间进行角色翻转。如何进行对象晋升机制?
一轮GC
还存活的新生代需要晋升。当对象从From
空间复制到To
空间时,若To
空间使用超过 25%,则对象直接晋升到老生代中。设置为25%的比例的原因是,当完成Scavenge
回收后,To
空间将翻转成From
空间,继续进行对象内存的分配。若占比过大,将影响后续内存分配。分代回收-202209041934460.png
老生代
标记-清除算法
存放生命周期长的对象老生代使用标记-
标记-清除算法
- 对老生代进行第一遍扫描,
标记
存活的对象 - 对老生代进行第二遍扫描,
清除
未被标记的对象 ⇒ 碎片化问题- [1,2,3,4,5,6] ⇒ 2/4 ⇒ [1,3,5,6]
- 将存活对象往内存的一端
移动
- 清除掉存活对象边界外的内存(
压缩
)
- 对老生代进行第一遍扫描,
标记-清除是一种停下应用程序的技术,增量标记 - 提升标记性能
标记-清除需要停下应用程序,但老生代对象多,工作量大,所以v8使用
增量标记
的方式,防止停止时间过久;何谓
增量标记
为了解决全停顿的现象,2011年V8推出了增量标记;
简单理解·增量标记:V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直至标记完成。深入理解·增量标记:为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一“步进” 就让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成
清除算法-202209041935394.png
延展:Scavenge算法
Scavenge 主要采用了 Cheney算法,Cheney算法新生代空间的堆内存分为2块同样大小的空间,称为 Semi space,处于使用状态的成为 From空间 ,闲置的称为 To 空间。垃圾回收过程如下:
- 检查From空间,如果From空间被分配满了,则执行Scavenge算法进行垃圾回收
- 如果未分配满,则检查From空间是否有存活对象,如果无存活对象,则直接释放未存活对象的空间
- 如果存活,将检查对象是否符合晋升条件,如果符合晋升条件,则移入老生代空间,否则将对象复制进To空间
- 完成复制后将From和To空间角色互换,然后再从第一步开始执行
参考: