一、v8数据存放

在程序运行时,存放数据地方主要分为堆内存和栈内存

栈内存:因为使用的栈结构设计,所以叫栈内存,是一块连续的内存空间,处存储简单的数据,如基本类型(number、boolean、string)的值,复杂类型的堆内存指针也会存储在此,它是一个key、value的储存类型。栈内存的垃圾回收由系统自动直接管理,进行分配以及释放。堆内存:是一块不连续的内存空间,效率不如栈,可以存储栈内存的所有能储存的数据并且存储object数据。需要v8引擎主动去进行垃圾回收,这篇文章讲解的也是堆内存的垃圾回收(栈内存已经是系统自动管理了)。其中堆内存分为了

新生代(New Space):最开始我们定义一个对象会先放入这里,一系列回收后还存在就会被放到老生代中 老生代(Old Space):存储一些在新生代中经久存活(活动)的对象 和全局变量如window、docuement等。 大对象空间(Large object space):存储一些超大对象,比如创建一个99999长度每个内容都有值的数组Array,那么此变量就会在这里创建存储,回收过程与老生代相同。 代码空间(Code-space):编译器存储编译(JIT)JavaScript代码后得到的代码的地方。 细胞空间,属性细胞空间,map 空间(Cell space, property cell space, and map space):这些空间分别包含:Cells, PropertyCells, 和 Maps。这些空间中的每个空间都包含相同大小的对象,并且对它们指向的对象有一些限制,从而简化了回收。

本文主要解析新生代(New Space)与老生代(Old Space)的GC过程。

看一个简单的例子,他们在内存中的分配

const a = 1;

const b = 'bbbbb';

const c = {

d: 'ddd'

}

const e = () => {}

二、新生代、老生代

在垃圾回收设计中有个关键词:代际假说 **代际假说:**绝大部分的变量,在内存中存活(实际使用)的时间很短,比如函数运行完后就抛弃的变量;而大部分经久不死的变量,都是用得很久的变量,存活时间很长。

那么v8的垃圾回收也是这样设计的,把垃圾分为:新生代(短命)、老生代(长命),而且对于新生代而言,他们都是“短命鬼”,所以设计之初,新生代就不会分配太多内存空间给它们,所以新生代一般只有1-16M(按用时分配和系统类型32位or64位,32位系统8M以下,64位系统16M以下)容量(也是为效率考虑),而老生代拥有32位:700M,64位:1400M内存。

注:当然这个只是默认的情况,在使用V8运行环境时,我们可以自己设置

下图是新生代&老生代的模拟,可以看到新生代中分为了两块:to、from,这涉及了新生代垃圾回收算法的运行过程,请继续向下看。

各自算法

空间算法新生代Scanvenge(置换)老生代先用Mark-Sweep(标记清除),后用Mark-Compact(标记整理)

三、新生代:Scanvenge置换算法

这种算法主要采用置换的方式来进行垃圾回收。

将堆内存中的空间一分为二,每个都被成为semispace(半空间),只有一个空间会被激活使用,当需要进行垃圾回收时,将会标记处正在使用的变量,移动到另外一块,然后将以前那块完全清空。to和from并不指定是哪一块,当前处于使用中的那块semispace为from,即将被使用的块称为to。进行标记后,将正在使用的变量,移动到to后,旧的块会完全清除释放,并闲置,此时to也将称之为from,已经这一块正在被使用。这样做还可以避免产生内存碎片,每次复制后新的半区分配的内存地址总是连续的。由于每次都会进行复制移动操作,性能开销会比较大,所以不能把新生代空间设置的很大,所以默认才会有1-16M(按用时分配和系统类型32位or64位,32位系统8M以下,64位系统16M以下)容量(也是为效率考虑的大小,减少操作空间换时间。

红色代表正在使用的对象

确定活动对象:全遍历,确定所有有引用的活动对象(图中红色)。

现在新生代共有a、b、c3个对象,触发新生代的垃圾回收时,a被检测到已经无用了,此时to的semispace被激活.b、c将被复制到to,内存地址指向改变,to区成为from区,完成一次新生代垃圾回收第二次垃圾回收被激活,此时b对象也已无用那么重复过程将c对象复制到to,内存地址指向改变,to区成为from区。当一个活动对象在第二次垃圾回收过程还是被检测到是活动的,并且此时semispace的内存已被占到25%以上,那么就会将此对象便会晋升移动到老生代中。

四、老生代:Mark-Sweep(标记清除)、Mark-Compact(标记整理)

在老生代中Scanvenge置换算法并不使用于老生代中,对于分配大内存区域,置换速度会很慢,1000M的内存进行一次Scanvenge算法垃圾回收,会使浏览器卡顿5S以上的时间(视电脑性能),所以要使用别的策略去管理老生代。

Q:为什么会有两种算法 A:因为是先Mark-Sweep(标记清除)后,没有像新生代空间中会移动,而是直接清空无用对象,那么此时分配的内存中会有很多非连续空间,不利于后续新对象分配,所以还需要进行一次Mark-Compact(标记整理),将还在存活的对象想队前靠,更换直接,整理内存碎片。

确定活动对象:全遍历,判断此对象在栈中能反推出来,确定标记所有引用的活动对象(图中红色)。

标记后,清除所有无用对象开始整理,将活动对象向头端移动,移动分配新的内存地址。

五、过程优化

垃圾回收对于v8来说是一个非常耗费性能的事情,如果进行一次非增量(新老生代全扫描)垃圾回收,系统会有1S全停顿(stop-the-world)时间(js线程卡住,等待垃圾回收完毕再继续运行),这对于浏览器是不可接受的,如果有动画,则会造成动画不流畅,所以,工程师对此进行了多项优化。

1. 增量标记(Incremental marking):

在老生代中,由于占用内存很高,进行垃圾回收时性能开销很大,尤其是在标记阶段全遍历,最坏的情况要遍历标记将近1400M的数据,想象一下都觉得恐怖~ 所以在标记阶段,v8会将本该一次遍历的数据的大任务,分成多个小任务分段进行,每次进行完一个小任务就让js线程运行一会,相互交替运行,这有点像HTML5的新api时间分片技术MDN-window.requestAnimationFrame

下图说明运行过程

2. 延迟清除(Lazy sweeping):

在增量标记完成后,在清除阶段也有优化。此时会进行无用对象的清除,v8会视情况js进程空闲进行一次清除或直接分多次清除。

3、多线程并行(Parallel)

这是配合增量标记的一种方法,开启多个辅助线程执行一部分任务,减少主线程时间。这样也带来一些开启线程额外开销。

总结

本文从堆栈入手,简单谈了一下新生代老生代的各自垃圾回收的方式以及其优化,当然实际回收过程做的事情会复杂很多,这里不过多详解,v8垃圾回收机制非常复杂细节超级多,大家有兴趣可以再去了解学习深入。文中如有错误,请私信或评论指正。

查看原文