# JS v8 引擎内存简述

# 简介

V8引擎是一个JavaScript引擎实现,最初由一些语言方面专家设计,后被谷歌收购,随后谷歌对其进行了开源。V8使用C++开发,,在运行JavaScript之前,相比其它的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生机器码(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript程序在V8引擎下的运行速度媲美二进制程序。V8支持众多操作系统,如windows、linux、android等,也支持其他硬件架构,如IA32,X64,ARM等,具有很好的可移植和跨平台特性。chorme 以及 Node.js 都在使用 v8 引擎。

# 内存管理

javascript的内存空间分为堆(Heap)和栈(Stack)。堆是一个树形结构的数组。栈是一个数组结构,遵循“先入后出”的原则

#

栈是一个临时变存储空间,在javascript中主要存储局部变量和函数调用。

基础数据类型的变量都是直接存储在栈中,如String、Number、Boolean、Null、Undefined、Symbol等等。复杂类型数据会将对象的引用(实际存储的指针地址)存储在栈中,数据本身存储在堆中。

每个函数的调用时,js解释器都会现在栈中创建一个调用栈(call stack)来存储函数的调用流程顺序。然后把该函数添加进调用栈,解释器会为被添加进的函数再创建一个栈帧(Stack Frame),这个栈帧用来保存函数的局部变量以及执行语句)并立即执行。如果正在执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈并执行。知道这个函数执行结束,对应的栈帧也会被立即销毁。

#

堆一般用来存储比较复杂的数据对象。同时也被划分为了代码区(code space)、map区(map space)、大对象区(large object space)、新生代(new space)、老生代(old space)等等。

# 内存构成

在浏览器(chrome)中,js运行时,内存一般都是由如下几部分组成:

  • 代码区(code space)
  • map区(map space)
  • 大对象区(large object space)
  • 新生代(new space)
  • 老生代(old space)

# 代码区(code space)

存放预编译代码。

# map区(map space)

存放对象的Map信息。每个Map对象固定大小,为了快速定位,所以将该空间单独出来。

# 大对象区(large object space)、

为了避免大对象的拷贝,使用该空间专门存储大对象。包括Code、FixedArray等等。

# 新生代(new space)

大多数对象创建时都会被存储到该区域。主要由两个semispace(半空间)构成,from区域、to区域。内存最大值在64位系统和32位系统上分别为32MB和16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法。

对象创建时都会被分配在from区域,然后在gc(Garbage Collection)阶段,会将from中还存活的数据复制到to区域,然后交互to区域和from区域。实现一次垃圾回收。如果在两次gc中都还存活的数据就会被移动到老生代区域存储。

新生代的gc频率很高、速度快、但是空间利用率底,典型的空间换时间的做法。

# 新生代中的数据晋升

当一个对象在经过多次复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种对象从新生代转移到老生代的过程我们称之为晋升。

在新生代中的数据晋升需要满足下面两个条件中的任意一个:

  1. 对象是否经历过一次Scavenge算法,如果是则放入老生代数据中
  2. to区域的剩余的内存占比是否已经低于25%,如果是则放入到老生代中

# 老生代(old space)

新生代中多次回收仍然存活的对象会被转移到空间较大的老生代。因为老生代空间较大,如果回收方式仍然采用 Scanvage 算法来频繁复制对象,性能开销会非常大,不适用于这种场景。

现在在老生代中主要适用“标记清除”(Mark-Sweep)来进行gc。在以前使用“引用计数”来进行gc,但是该方式容易造成内存泄漏,主要表现为循环引用的情况下,数据不能被正常的回升,到是内存飙升,造成内存泄漏。

“标记清除”(Mark-Sweep)主要分为两个阶段:标记、清除。

# 标记阶段

在标记阶段会遍历堆中的所有对象,然后标记活着的对象。Sweep算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收,具体步骤如下:

  1. 垃圾回收器会在内部构建一个根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。

  2. 垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。

# 清除阶段

该阶段则是将标记阶段中不能访问的变量进行清理。

# Mark-Sweep 优化

Mark-Sweep算法存在一个问题,就是在经历过一次标记清除后,内存空间可能会出现不连续的状态,因为我们所清理的对象的内存地址可能不是连续的,所以就会出现内存碎片的问题,导致后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,而这次垃圾回收其实是没必要的,因为我们确实有很多空闲内存,只不过是不连续的。

为了解决这种内存碎片的问题,Mark-Compact(标记整理) (opens new window)算法被提了出来,该算法主要就是用来解决内存的碎片化问题的,回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的另一端进行移动,移动完成后再清理掉边界外的全部内存。

# 增量标记(Incremental Marking)

由于JS的单线程机制,垃圾回收的过程会阻碍主线程同步任务的执行,待执行完垃圾回收后才会再次恢复执行主任务的逻辑,这种行为被称为 全停顿(stop-the-world) 。在标记阶段同样会阻碍主线程的执行,一般来说,老生代会保存大量存活的对象,如果在标记阶段将整个堆内存遍历一遍,那么势必会造成严重的卡顿。 因此,为了减少垃圾回收带来的停顿时间,V8引擎又引入了Incremental Marking(增量标记) (opens new window)的概念,即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。这个理念其实有点像React框架中的Fiber架构,只有在浏览器的空闲时间才会去遍历Fiber Tree执行对应的任务,否则延迟执行,尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。

得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记和并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。

# 参考