浏览器的垃圾回收机制

142 阅读5分钟

在谈垃圾回收之前我们先谈一下内存的分配,如果内存分配的很散,而不是集中分配会有什么问题?即“内存碎片”问题,会影响后续大内存对象的存放。 优化之前的浏览器垃圾回收算法有什么问题?

背景

为什么浏览器要设置垃圾回收机制:

在chrome浏览器中,javascript中的V8引擎可以使用的内存大小是被限制的。且不同操作系统对应分配的内存大小就不同(针对32位和64位的操作系统),内存范围大致在 0.7G-1.4G。

1.浏览器本身是web应用,不考虑持久化缓存,所以就需要一个垃圾回收机制,来实现变量的用完即抛,节省内存;

垃圾回收机制需要解决的问题:阻塞问题。
因为JS引擎是单线程,而垃圾回收会阻塞JS的线程。目前V8一次回收1.5G的堆内存占用时间是50ms,因此如果内存过大,那么垃圾回收的时间就会增长,造成阻塞时间增长。

一、V8之前的垃圾回收

1.引用计数

语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
引用计数算法优点:

  • 引用计数为零时,发现垃圾立即回收;
  • 最大限度减少程序暂停;

引用计数算法缺点:

  • 无法回收循环引用的对象;
  • 空间开销比较大;

2.标记清除

按照变量是否进入“环境”,如果离开执行环境就认为是垃圾。因此,这样就能解决引用计数方式中循环引用的问题,因为两个循环引用的变量都离开了执行环境作用域。但是对于内存碎片的问题并没有解决。 分标记和清除两个阶段完成。 对比引用计数算法,标记清除算法最大的优点是能够回收循环引用的对象 问题:内存碎片问题

二、V8之后的垃圾回收

将需要回收的垃圾数据进行分类:新生代和老生代。

2.1 分代回收

为了更快的提高垃圾回收的效率,V8引擎针对不同的数据采用不同的回收方式。按照数据存活时间长短将数据划分为两类:临时变量 和 长期对象

临时对象
通常函数中声明的变量,块级作用域中声明的变量,这些变量可能很快就不再使用了,因此为了保证后续有足够内存接纳新变量,所以要对已存在的临时变量增加回收频率。 长久对象
全局对象,事件回调

针对这两类不同的数据,存储在不同的存储空间中,对应着不同的回收策略,将存在临时变量的区域称为新生代区域 - 对应回收策略:副垃圾回收,将存储长期变量的区域称为老生代区域 - 对应垃圾回收策略:主垃圾回收器。

2.1.1 新生代存储空间的回收策略 - 【scanvenge算法】

针对生命周期短的数据,主要的策略是:使用低内存空间的同时,提高回收频率。因此给新生代的数据分配了64MB的存储空间。
回收算法:scanvenge复制算法
将64MB分为两个部分,from和to分别作为使用和缓存复制区域

优点:回收快 缺点:至少一半的空间是要等待复制的,造成浪费。

2.1.2 老生代存储空间的回收策略 - 【标记-清除】

怎么判定老生代:一轮GC还存活的新生代需要晋升为老生代。 老生代不能采用“复制算法”,因为 Mark-Sweep标记清除算法
过程、缺点&存在问题
Mark-Compact标记整理算法
解决的问题
过程
存在缺点

STW全停顿(Stop The World)
在垃圾回收并发机制实现之前,浏览器的程序执行和垃圾回收执行是同步串行的(即,当执行垃圾回收的时候,程序需要暂停,等待垃圾回收执行完毕后再继续执行。之所以这么设计,是因为:解决垃圾回收时候,主程序动态有同时变更有产生垃圾,导致垃圾不一致的问题。如果一次垃圾回收执行50ms,那么主程序就需要等待50ms),这很容易导致页面的卡顿。
问题:同步阻塞问题

增量回收 为了减少中间停顿的过程,提出了增量回收,这里有点类似于react中执行diff算法思想。就是每隔一个时间段只执行执行一部分垃圾回收,这样就避免了每次停顿进行全部垃圾处理的时间。虽然这种想法解决了中间停顿时间长的问题,但是本质上总的垃圾回收时间没有变(依然没有解决同步阻塞问题),此外还带来另外一个问题:中断/恢复如何记录上一个中断点? 问题:如何记录中断/恢复中的“中断”点

并行标记 && 三色标记法
为了解决STW中同步协调导致阻塞的问题,提出了并行执行垃圾回收的想法。并行垃圾回收可以分为:并行标记 && 并行清理
并行标记:在主程序执行的时候,异步对内存进行垃圾标记。这个解决的是同步阻塞的问题
三色标记法:这个是解决增量回收中,记录中断、恢复中的中断点

三色标记法执行过程