JavaScript垃圾回收

859 阅读8分钟

内存管理

在编写代码时如果不过了解内容管理的机制,就很有可能写出一些不容易察觉,造成内容泄露的代码

内存:由读写单元组成,表示一片可操作的空间

内存管理:代码编写时去借助一些API开发者主动的去操作内存空间的申请、使用和释放

管理流程:申请——使用——释放

JavaScript中的内容管理也是 申请、使用、释放三个步骤,但是ECMAScript没有提供让开发者直接操作内容空间的API,但可以通过一些方式,主动触发程序去操作内存空间

// 简单的内存管理
// 申请
let obj = {}
// 使用
obj.name = 'abc'
// 释放
obj = null

JavaScript中内存管理是自动的

找到这些符合这些条件的对象,标记为垃圾,让垃圾回收去回收这些内存

  1. 对象不再被引用
  2. 对象不能从根上访问到(不可达)

可达对象:可以从根(全局变量对象)出发可以被访问到的对象

GC

GC是垃圾回收机制的简写

GC可以找到内存中的垃圾,并释放和回收空间

GC回收的垃圾:程序中不再使用的对象和不能再访问到的对象

GC算法:工作时查找和回收所遵循的规则

常见的GC算法

  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收

引用计数算法

核心思想:为对象引用设置引用数,进行维护,当引用关系发生变化时修改引用数字;判断当前引用的引用计数是否为0,为0时GC就把这个对象的空间进行回收和释放

const obj = { name: 2 }
const obj1 = obj
const obj2 = obj
// { name: 2 } 被obj、obj1、obj2 引用

// obj1 不再引用  { name: 2 } 引用数-1
obj1 = null

var b = 123
function fn () {
    b = 456
    // a 是const声明变量 是块级作用域
    // fn执行完 a的引用就是0
    // 会被GC回收
    const a = 'abc'
}
fn() // GC回收a

优点

  • 发现垃圾时立即回收:当引用数为0时证明这个对象是垃圾,可以立刻回收
  • 最大限度减少程序暂停:当内存不足时,引用计数算法可以迅速清理引用计数为0的对象,减少程序暂停,减少程序的卡顿

缺点

  • 无法回收循环引用的对象:对象循环引用就会导致引用数不能为0,无法被GC回收

  • 资源开销大:用额外维护引用计数器,引用数变化时要维护引用计数

    function fn () {
        const obj1 = {}
        const obj2 = {}
        // obj1属性引用了obj2 obj2属性引用了obj1
        // 导致obj1和obj2的引用数都为 1
        obj1.name = obj2
        obj2.name = obj1
    }
    // fn执行后 const 声明的obj1和obj2的引用数应该为0
    // 但因为它们相互引用导致 引用都不为0 GC不会回收
    fn()
    

标记清除算法

核心思想:将垃圾回收操作分为标记和清除两个阶段

  • 标记:遍历所有对象,对活动对象(可达对象)进行标记
  • 清除:遍历所有对象,清除没有标记的对象,把第一阶段的标记去掉,方便下一次标记

优点

  • 标记清除算法可以解决循环引用的问题,因为它的依据是可达对象,已经不可达的循环引用不会被标记

缺点

  • 空间碎片化:清除的空间不能最大化的使用;标记清除算法所清除后的空间内存地址不一定是连续的,是分散并且体积不大;当空闲链表要使用这些内存地址有可能发生清除后的空间位置不满足申请的大小要求,这个空间位置没有被用上,空间上的浪费
  • 不是立即回收垃圾对象,要先循环做标记后,再循环回收

标记整理算法

可以认为是标记清除算法的增强版,分为标记和整理

  • 第一阶段的标记操作和标记清除是一样的
  • 第二阶段会先对所有的对象进行内存地址整理,把标记对象和没有标记对象分别进行内存地址整理,让内存空间尽可能是连续的,再把没有标记的对象进行清除并且去掉当前的标记

优点

  • 减少碎片化空间: 会在清除前做内存整理再清除

缺点

  • 和标记清除一样 不会立刻回收垃圾

V8引擎的垃圾回收算法

是目前主流的JavaScript执行引擎

采用及时编译:直接将代码转成可以执行的机器码,速度快

内存设限:64位系统是不超过1.4G 32位系统是不超过700M

采用分代回收的思想:将内存安装规则划分为新生代和老生代,再对新生代和老生代分别采用不同的垃圾回收算法

V8主要用到垃圾回收算法

  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量

V8新生代垃圾回收

V8将内存划分为两份,小的空间区域是新升代对象(64位系统是32M,32位系统是16M)

新生代对象:存活时间比较短的对象,例如局部作用域的对象

新生代对象回收采用 空间复制和标记整理

  • 将新生代内存空间划分为两个大小相等的空间
  • 活动对象使用的空间为From,空闲空间为To
  • 申请空间时,将全部活动对象存储在From,To是空闲的
  • 当From空间不足时,会触发GC操作通过标记整理(标记From的活动对象,整理对象,清除垃圾)后将活动对象拷贝至To空间,备份了From里面的活动对象
  • 再将From空间进行完全释放(将From的活动对象,拷贝到To,清理From,完成交换空间)

在进行交换空间拷贝时发现新生代对象使用空间在老生代对象里面也有出现就会出现晋升操作

晋升:将新生代对象移动到老生代区域进行存储

晋升的判断条件

  • 一轮GC后还存活的新生代对象需要晋升
  • To空间使用率超过25%时,To空间里全部的新生代对象都需要晋升

V8老生代对象回收

老生代对象:存活时间较长的对象,全局对象

老生代空间大小:64位系统1.4G;32位系统700M

老生代对象回收采用 标记清除、标记整理、增量标记算法

空间清理——标记清除算法:用标记清除对活动对象标记,清除空间(大部分情况下都用标记清除,速度快,空间碎片化问题不考虑)

空间优化——标记整理算法:发生晋升(新生代对象移动到老生代),如果老生代空间不足够存放时,用标记整理算法把之前标记清除创建的碎片化空间进行整理,整理出大的空间

效率优化——标记增量算法:将一次完整的标记动作分成多次执行;因为GC工作时,js程序需要暂停,所以把标记动作一层层对象来分时段标记后再整体清除,减少GC工作时js程序长时间的暂停

标记增量算法

因为垃圾回收执行时js代码的执行要停下来等待,通过标记增量算法将一次垃圾回收操作分开几个小的操作片段来执行

标记增量算法执行时会遍历所有对象,这时可以先只对第一层的可达对象进行标记(先不递归子对象)后,让程序进行执行一会,再进行下一层子对象的可达标记后再让程序执行,以此来交替标记操作和程序执行;当标记完成后,再正常地暂停程序,进行清除回收

虽然标记增量会出现程序的多次停顿,但是V8引擎可以快速完成,相比于较长的整个垃圾回收操作,这样标记增量会更好,程序的停顿不明显

新生代与老生代对比

新生代区域垃圾回收是用空间换时间,是用复制算法,保证有一个空闲的To空间存在;因为新生的内存空间较小,所以可以用复制算法

老生代区域内存空间比较大,并且对象比较多,所以不适合用复制算法