JavaScript 垃圾回收机制

1,548 阅读5分钟

GC 算法

GC 算法就是垃圾回收机制的简写,GC 可以找到内存中的垃圾,并且释放,回收空间

GC 中认为是垃圾的标准

  1. 程序中不在需要使用的对象,当函数执行结束后,name就不在被使用,因此也被认为是垃圾

例如:

function func(){
name = 'silan'
return `${name} is a coder`
}
func()

2 . 程序中不能再访问到的对象,执行完func(),函数外部就访问不到name,因此也被认为是垃圾

function func(){
const name = 'silan'
return `${name} is a coder`
}
func()

常见的 GC 算法

  1. 引用计数
  2. 标记清除
  3. 标记整理
  4. 分代回收

引用计数算法实现原理

核心思想就是设置引用数,判断当前引用数是否为 0,通过一个引用计数器,当引用关系发生改变的时候,就会修改引用计数器的数字

引用关系改变就是说,有个对象,当有个变量指向它时,引用计数加 1,以此类推。让引用数字为 0 的时候立即进行回收

来看下面的代码,nameList 里面每个数组元素都是 user1,user2,user3 对象里面的属性,因此 user1,user2,user3,引用次数都为 1

fn 函数体里面的 num1 ,num2 因为是在函数体里面,没用使用到,因此引用次数为 0,但是 num3,num4 是挂载在全局对象上,可以通过全局对象访问到,因此引用次数为 1

const user1={age:12}
const user2 = {age:13}
const user3={age:14}
const nameList=[user1.age,user2.age,user3.age] //引用次数都为1
function fn(){
  const num1= 1 // 引用次数0
  const num2 =2 // 引用次数0
  num3=3 //引用次数1
  num4=4 //引用次数1
}
fn()

其实还有个更简单的方法,编辑器已经告诉我们num1 已经被定义但是没有使用过,因此引用计数为 0

优缺点:

优点:

  1. 发现垃圾时立即回收
  2. 最大限度减少程序暂停,当发现内存即将到临界点的时候,就开始进行引用计数清除。

缺点:

  1. 时间复杂度比较高
  2. 无法回收循环引用的对象,比如 a.test =b , b.test =a
  3. 资源消耗大

标记清除实现原理

实现原理有两个阶段,

  1. 先遍历所有活动对象,做上标记
  2. 遍历所有对象清除没有标记的对象

最后回收相应的空间

标记清除优缺点

优点:

  1. 可以解决循环引用问题

缺点:

  1. 不会立即回收垃圾对象,即使发现了垃圾对象,也要等到遍历完才进行清除,会导致程序卡顿

  2. 产生空间碎片化,不能得到空间最大化效率的使用

可以看下这张图,假设 A 对象和 C 对象都没有变量引用,因此标记清除算法认为他们是可回收空间,回收之后正好是 3 个域空间,等下次程序申请空间的时候,在分配出来,但是 b 对象空间和 c 对象空间正好被 a 对象隔开,因此当程序申请 3 个空间时,就会发现两个域空间都不够,并且不连续。

这就是空间碎片化问题

标记整理算法

标记整理算法可以看做是标记清除算法的增强,标记整理算法流程跟标记清除算法差不多

  1. 遍历所有对象,给活动对象打上标记
  2. 遍历所有对象, 整理空间,根据标记移动对象位置,让地址上产生连续
  3. 移除非活动对象,回收空间

优缺点

优点:

  1. 减少碎片化空间

缺点:

1.不会立即回收垃圾对象,跟标记清除一样存在的问题

V8 引擎回收策略 (分代回收)

V8 是一款主流的 JavaScript 执行引擎,V8 采用即时编译,但是内存设限,64 位操作系统 1.5G,32 位系统 800M。也是因为内存有限制的原因,在回收策略上面使用了分代回收

分代回收,就是把内存分为新生代,老生代,小空间用于存储新生代对象,在不同系统上面也有不同的大小(64 位 32M,32 位 16M),然后针对不同对象采用不同 GC 算法

新生代对象:指的是存活时间比较短的对象,比如局部变量

老生代对象: 指的是存活时间比较长的对象,比如全局作用域下的变量,和闭包

v8 常用的 GC 算法

  1. 分代回收
  2. 空间复制
  3. 标记清除
  4. 标记整理
  5. 增量标记

v8 如何回收新生代对象

from 空间就是活动空间,to 为空闲空间,活动对象都存储于 from 空间,这两个空间是等大的。

一但触发垃圾回收时,先讲 From 空间进行标记整理,然后将活动对象拷贝至 to 空间,接着释放 from 空间,最后在进行 from 和 to 的空间交换,原来的 from 就变成了 to,原来的 to 就变成了 from,这也就是空间复制算法。

值得注意的是,在进行复制算法的时候,有可能会存在对象晋升,也就是将新生代对象移动至老生代对象存储空间,晋升有两种触发条件:

  1. 一轮 GC 操作后还存活的新生代对象

  2. 当 to 空间使用率超过 25%的时候,因为复制算法是要把使用空间里面的活动对象拷贝至 to 空间,当使用空间超过 25%,to 空间也超过 25%,那么就存放不下了。

v8 如何回收老生代对象

老生代对象存储于老生代对象存储区域,见上图。大小在不同系统上面也有所不同,62 位为 1.4G,32 位为 700M

主要采用标记清除算法进行垃圾回收,当新生代对象晋升到老生代存储区域时,如果空间不足,就会进行标记整理操作,优化空间,最后采用增量标记提高效率

因为垃圾回收是会阻塞程序运行,因此垃圾回收和程序运行交替进行,没有一次性进行标记清除,而是在程序运行空隙进行增量标记,然后在标记完成后在进行垃圾回收,目的是为了尽可能小的去打断程序运行,增加效率

本文使用 mdnice 排版