浅述垃圾回收机制

| 1.5k字 | 5分钟

背景

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空间 一次垃圾回收 ♻️ 分为两步:

      1. 将 from 空间的活对象(从 roor 可以访问到)复制到 to
      2. 切换 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 空间。垃圾回收过程如下:

  1. 检查From空间,如果From空间被分配满了,则执行Scavenge算法进行垃圾回收
  2. 如果未分配满,则检查From空间是否有存活对象,如果无存活对象,则直接释放未存活对象的空间
  3. 如果存活,将检查对象是否符合晋升条件,如果符合晋升条件,则移入老生代空间,否则将对象复制进To空间
  4. 完成复制后将From和To空间角色互换,然后再从第一步开始执行

参考:

  1. 浅谈JS内存机制
  2. Trash talk: the Orinoco garbage collector
  3. 一起来看Javascript的垃圾回收机制