浅析JavaScript的垃圾回收机制

177 阅读4分钟

前言

了解垃圾回收机制,大概使用较为广泛的有 引用计数法标记清除法两种,而引用计数法已经几乎被淘汰,现在大都使用标记清除法.因此,我们在了解时,引用计数法就简单说一下,重点说标记清除法

引用计数法(Reference-Counting)

一.原理

引用计数法的原理,是在每个对象身上加上一个标记,当对象被引用时,标记计数加一,当标记的计数为0时,执行垃圾回收器

二.缺陷

引用计数法作为已经被淘汰的回收机制,存在一个严重的缺陷: 当两个对象相互引用时,他们的标记计数永远不可能为0,这样,当函数执行结束之后,因为这些互相引用的对象没有被实际调用,但又无法清除,处于一个 "无法被触及的孤岛" ,想要提高性能,必须手动在函数结束时将其设置为空值

标记清除法(Mark-Sweep)

一.原理

从根节点向子级作用域逐级深入,寻找变量并标记对应的堆空间,在遍历完所有作用域后清除未被标记的堆空间,从而解决了循环引用的问题

二.缺陷

标记清除法因为要遍历整个DOM树,显然要消耗大量的性能,因为JavaScript是单线程的,执行垃圾回收器的时候更是要陷入 Stop-The-World 的窘境,所有程序的运行都需要等待垃圾回收器的执行完毕才能继续,这显然不合理,说到这就不得不说引用计数法在性能和效率上突出的优点了,虽然被淘汰,但它并非是一无是处的

三.优化

标记清除法的缺陷很明显,但好在有很多优化的办法,其中最广泛的分为三种:

1.分代优化

我们可以发现,大部分被创建的对象生命周期都不长,而生命周期长的对象,它往往更不容易被销毁,这是分代优化的基础

在JavaScript中,堆内存被分为新生代(new-space)和老生代(old-space)

  • 新生代存储的对象是刚被创建或仅经历过一次垃圾回收存活的对象;
  • 新生代的空间很小,默认64位系统只有32M的空间;
  • 新生代又被分为两个小空间(semispace),分别是对象区域空闲区域,新的对象会被存储在对象区域中,当对象区域存储快满时,就会执行垃圾回收机制,需要销毁的对象会被上标记,最后清理掉,而存活的对象会被复制到空闲区域,它们会有序排列,从而完成内存中的碎片整理,而回收的最后,对象区域会和空闲区域交换身份
  • 因为新生代空间小,清理频繁,所以必须让不容易销毁的对象单独拿出来,这就有了对象晋升策略,即在垃圾清理中存活达到2次的对象,会进入老生代
  • 这种垃圾清理方式,被称为Scavenge算法

老生代的对象要么比较大,要么活得久,且老生代的空间较大,64位的系统默认为1400M,清理老生区,使用就是标记清除法

2.增量收集

如果有许多对象,并且每次执行垃圾回收都要从根开始遍历,会导致性能的大量消耗,因此,现在的浏览器引擎会将整个对象拆分为多个部分,然后将这些部分逐一清除.这样就会有许多小型的垃圾收集,而不是一个大型的.这需要他们之间有额外的标记来追踪变化,将一个大的延迟转为均匀的微小延迟,提高程序流畅程度

3.闲时收集

垃圾回收器会尽量在CPU空闲时运行,减少对代码执行的影响

最后说一下,标记清除法执行多次之后,会导致内存中的不连续空间(空间碎片)变多,从而导致存储较大的对象时可能出现内存溢出的错误,为此, 标记整理法(Mark-Compact) 应运而生
该算法与标记清除法前期的准备差不多,先标记,然后让所有存活的对象朝一个方向移动,以整理内存