V8引擎垃圾回收原理解析

在过去很长一段时间内,JavaScript开发者很少遇到需要对内存进行精确控制的场景,也缺乏控制的手段,说到内存泄漏,大家可能首先想到早期浏览器中的卡顿问题,如果内存占用过多,基本等不到代码进行垃圾回收,用户已经开始不耐烦的刷新网页了。

随着node的发展,JavaScript已经实现了Commonjs大统一的梦想,JavaScript的应用场景早已不再局限在浏览器中,在浏览器中那些短时间执行的场景中,由于运行时间短,而且运行在用户的机器中,随着进程的退出,内存会释放,几乎没有内存管理的必要。但是,随着node在服务端的广泛应用,在其它语言里存在的问题在JavaScript中也逐渐暴露出来了。

我们在学习JavaScript的时候听说过垃圾回收机制,JavaScript就是由垃圾回收机制来进行自动内存管理的,这使得开发者在编写JavaScript的时候,不需要像其它语言那样时刻关注内存分配和释放的问题。只在浏览器中进行开发时,几乎很少有人遇到因为垃圾回收对项目构成性能影响的情况,Node极大的拓宽了JavaScript的应用场景,当应用场景从浏览器延伸到各种场景中时,我们就能发现,内存管理的好坏、垃圾回收状态的优良与否至关重要,而不管是在浏览器环境、还是node环境中,这一切都与V8引擎息息相关。

1. V8的内存限制

在一般的后端语言中,在基本的内存使用上是没有限制的,但是在node中通过JavaScript使用内存时就会发现只能使用部分内存(64位系统约为1.4G,32位系统约为0.7G),在这样的限制下,node无法直接将一个大文件读入内存进行处理,即使电脑的物理内存有16G,在单个node进程的情况下,内存也无法得到充足的使用。

造成这个问题的原因在于V8引擎,所以node中使用JavaScript对象基本上都是通过V8自己的方式来进行分配和管理的。这套管理机制在浏览器中使用起来绰绰有余,足以胜任前端页面中的所有需求,但是在node环境中,却限制了开发者对大内存文件的分析和处理。

尽管在服务端操作大内存也不是常见的需求场景,但有了限制之后,我们的行为就如同带着镣铐跳舞,如果在实际应用中不下心触碰到这个界限,会造成进程退出,如果是在浏览器环境中,会导致浏览器白屏、或者卡死。只有在知晓其原理后,才能避免问题,更好的进行内存管理。(这段话摘自《Node深入浅出》,个人认为写的很好)

2. V8的对象分配

在V8中,JavaScript对象是通过堆来进行分配的。Node提供了内存使用量的查看方式,在node环境中输入以下代码:

console.log(process.memoryUsage());
复制代码

执行以上代码,将会得到输出的内存的使用信息(单位是字节):

1111111.jpg

在memoryUsage方法返回的参数中:

  • rss是resident set size的缩写,是进程的常驻内存
  • heapTotal是已经申请到的堆内存
  • heapUsed是当前使用的量
  • external代表绑定到Javascript对象的 C++ 对象的内存使用情况

当我们在代码中申明变量并且赋值时,所使用的对象的内存就分配在堆中,如果已申请的堆的空闲内存不够分配新的对象,将继续申请堆内存,知道堆的大小差超过V8引擎限制为止。

那么问题来了,V8为何要限制堆的大小?

  • 表层原因是JavaScript最初只运行在浏览器环境,几乎不会遇到大量使用内存的场景,所以对于网页来说,V8的限制已经绰绰有余
  • 深层原因是V8的垃圾回收机制。按官方的说法,以1.5G的垃圾回收的堆内存为例,V8做一次小的垃圾回收需求50ms以上,而做一次非增量式回收甚至需要1s以上,可见其耗时之久,而在这1s的时间内,应用的性能和响应时间会大大下降,这样的情况不仅后端无法接受,前端也无法接受,更重要的是,用户也无法接受。因此在当时的情况下,直接限制堆内存是一个好的选择。

当然了,这个限制并不是死的,V8为我们提供了方法,可以手动打开限制,从而让我们使用更多的内存:

在命令行中输入以下代码:node --v8-options,然后我们会在命令行窗口中看到V8的选项,这里我们可以看到下面几个选项:

222.jpg

在Node启动时,我们可以传递--max-old-space-size或者--max-new-space-size来调整内存限制大小,比如:

  • node --min-semi-space-size=1024 index.js 设置新生代内存中单个半空间的内存最小值,单位MB
  • node --max-semi-space-size=1024 index.js 设置新生代内存中单个半空间的内存最大值,单位MB
  • node --max-old-space-size=2048 index.js 设置老生代内存最大值,单位MB

上述参数在环境初始化时生效,一旦生效,就不能动态改变,只能手动调整,如果遇到内存不够的情况,可以用这个方法手动放宽限制,从而避免由内存问题引起的网页白屏或者奔溃,接下来让我们了解一下垃圾回收方面的策略,在限制的前提下,带着镣铐跳出的舞蹈并不一定就难看。(这段话摘自《Node深入浅出》,个人认为写的很好)

3. V8的垃圾回收机制

在展开介绍垃圾回收机制之前,有必要简略介绍下V8用到的各种回收算法

3.1 垃圾回收算法

V8的垃圾回收算法主要基于分代式垃圾回收机制,在早期的垃圾回收中,人们发现没有一种算法能够胜任所有的场景,因为在实际应用中,对象的生存周期长短不一,不同的算法只能针对特定的情况发挥作用。因此,在现代的垃圾回收算法中,根据对象的存活时间将垃圾回收进行了不同分代,主要分为新生代和老生代,然后对不用分代的内存使用不用的算法。

3.1.1 V8的内存分代

在V8中,主要将内存分为新生代老生代两种,新生代中的对象存活时间较短,老生代中的对象存活时间较长(或常驻内存中),如下图所示:

新生代内存空间 老生代内存空间

V8堆的整体大小就是新生代内存空间加上老生代内存空间,前面我们提到的两个命令行就可以用于设置这个空间的最大值,需要注意的是,这个最大值需要在启动的时候就指定,因此,V8使用的内存无法根据情况自动扩充,当内存分配过程中超过极限值的时候,就会引起进程出错,页面卡死,白屏。

3.1.2 新生代(Scavenge算法)

在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收,具体实现中采用的是Cheney算法,这是一种采用复制的方式实现的垃圾回收算法,具体过程是:

  1. 先将堆内存一分为二,每个内存空间称为semispace(半空间)
  2. 在这两个semispace中,只有一个处于使用中,另一个处于闲置中
  3. 处于使用状态的空间称为From空间,处于闲置状态的空间称为To空间
  4. 当我们分配对象时,先是在From空间中进行分配
  5. 开始进行垃圾回收时,会检查From空间中存活的对象
  6. 存活的被复制到To空间中,非存活对象占用的空间被释放
  7. 完成复制后,From空间和To空间的角色发生对换

简而言之,在新生代垃圾回收过程中,就是通过将存活对象在两个semispace空间之间进行复制,分代回收堆内存如下图所示:

新生代内存空间 老生代内存空间
semispace(From) semispace(To)

流程图如下:

  • 假设我们在From空间中分配了三个对象A、B、C
    aaa.jpg
  • 当程序主线程任务第一次执行完毕后进入垃圾回收时,发现对象A已经没有其他引用,则表示可以对其进行回收
    bbb.jpg
  • 对象B和对象C此时依旧处于活跃状态,因此会被复制到To空间中进行保存
    ccc.jpg
  • 接下来将From空间中的所有非存活对象全部清除
    ddd.jpg
  • 此时From空间中的内存已经清空,开始和To空间完成一次角色互换
    eee.jpg
  • 当程序主线程在执行第二个任务时,在From空间中分配了一个新对象D
    fff.jpg
  • 任务执行完毕后再次进入垃圾回收,发现对象D已经没有其他引用,表示可以对其进行回收
    ggg.jpg
  • 对象B和对象C此时依旧处于活跃状态,再次被复制到To空间中进行保存
    hhh.jpg
  • 再次将From空间中的所有非存活对象全部清除
    lll.jpg
  • 最后,From空间To空间继续完成一次角色对换
    iii.jpg
    可以看到,它的缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的,所以无法应用到所有的场景中,但是由于这个算法只复制存活对象,并且对于某些场景,存活对象只占少部分,所以它在时间效率上有优异的表现。所以说,Scavenge是典型的牺牲空间换取时间的算法。

需要注意的是:

  • 实际使用的堆内存是新生代中的两个semispace空间的大小和老生代所用内存大小的和。
  • 当一个对象经过多次复制仍然存活时,它将会被认为是生命周期较长的对象,这种对象随后会被移动到老生代堆内存中,采用新的算法进行处理,从新生代移动到老生代的过程被称为对象晋升

3.1.3 对象晋升(新 => 老)

在单纯的Scavenge算法中,From空间中的对象会被复制到To空间中去,然后对两个空间进行角色对换(又称翻转)。但是在分代式垃圾回收的前提下,From空间中的对象在复制到To空间时会进行检查。在一定条件下,将存活时间上的对象移动到老生代中,也就是完成对象晋升。

需要注意的是,满足对象晋升的条件主要有以下两个:

  • 对象是否经历过一次Scavenge算法
  • To空间的内存占比是否已经超过25%

这个晋升流程可以用以下的流程图来表示:

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享