Node与V8的垃圾回收机制

垃圾回收机制

  先来说说什么是垃圾回收机制和为什么需要垃圾回收,如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。不管是手动回收还是自动回收,在C和C++中程序员在编码过程中需要时刻关注内存的分配和释放问题,而在V8中采用了垃圾回收机制去自动回收,释放内存空间。

Node与V8的内存限制

  在一般的后端开发语言中,在基本的内存使用上没有限制,然而在Node中通过javascript使用内存时就会发现只能使用部分内存(64位系统下约为1.4G,32位系统下约为0.7G)。在这样的限制在,导致NodeJs无法直接操作大内存对象,比如无法将一个2GB的文件读入内存中进行字符串分析处理,及时物理内存有32GB。这样在单个Node进程的情况下,计算机的内存资源无法得到充分的使用。
  这个原因主要在于NodeJs基于V8构建,所以在Node中使用的javascript对象基本上都是通过V8自己的方式进行分配和管理的。所以导致了NodeJs在使用时如果不小心触碰了边界就会导致进程退出。

V8的对象分配

  在V8中所有的javascript对象都是通过堆来进行分配,当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已经申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超出V8的限制。
  V8之所以限制堆的大小,表层原因是因为V8最初是为了浏览器设计,不太可能遇到大量内存的场景。对于网页V8的限制绰绰有余。更深层次的原因就是V8的垃圾回收的限制。

  • 按照官方的说法,以1.5G的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至需要1s以上。这是垃圾回收中引起javascript线程暂停执行的时间,在这样的情况下,性能和响应能力都会直线下降,因此,当时的考虑下直接限制堆内存。
  • 当然这个限制可以打开,NodeJs启动时可以传递–max–old–space–size或者–max–new–space–size来调整新生代和老生代的内存限制大小。

V8的垃圾回收机制

先来说说V8的垃圾回收所用到的算法

V8主要的垃圾回收算法

  • V8的垃圾回收策略主要基于分代式垃圾回收机制。在自动垃圾回收的演变中,没有哪一种方式可以胜任所有的场景。因为在实际的应用中,对象的生存周期长短不一,不同的算法只能针对特定情况具有最好的效果。因此统计学在垃圾回收算法的发展产生了较大的作用,现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后针对不同的分代实施不同的算法。
  • V8堆的整体大小就是新生代和老生代所有之和的内存空间, 在V8的源码中可以看到,老生代的设置在64位系统下为1400MB,在32位系统下为700MB,对于新生代内存,又由两部分组成,都叫做reserved_semispace_size_组成,在64位和32位下一个reserved_semispace_size_分别为16MB和8MB。所以新生代内存的最大值在64位系统和32位系统上分别为32MB和16MB。同时V8堆内存的最大保留空间可以从源码中看到,公式为4 * reserved_semispace_size_ + max_old_generation_size_,因此V8在64位和32位系统上的最大内存分别为1464和732MB。
  • Scavenge算法
    • 新生代主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用了Cheney算法,Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,也就是之前说的semispace空间,在这个两个空间中,只有一个处于使用中,另一个处在闲置状态,处于使用状态的semispace空间成为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。复制完成后,From空间和To空间的角色发生对换。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制。
    • Scavege的缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异表现。
    • 由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模的应用到所有的垃圾回收之中。
    • 对象晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制。
      • 在默认情况下,V8的对象分配主要集中在From空间中。对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收,如果已经经历过了,会将该对象从From空间复制到老生代空间中,如果没有,则复制到To空间
      • 另一个判断条件是To空间的内存占用比。当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代空间中。
  • Mark-Sweep算法&Mark-Compact
    • Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,而Mark-sweep只清理死亡对象。活对象在新生代中只占较小部分,死对象在老生代中只占小部分,这是两种回收方式能高效处理的原因。
    • Mark-Sweep的最大问题在于一次标记清除回收后,内存空间因为回收的原因,导致了不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时候碎片空间无法完成分配,就会提前出发垃圾回收机制,这样的回收就是不必要的。为了解决这个问题,Mark-Compact被提出,Mark-Compact是标记整理的意思,是在Mark-Sweep的基础上演变而来的。它们的差别在于对象在标记为死亡后,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。
  • Incremental Marking算法
    • 垃圾回收时需要将应用逻辑暂停下来,这是为了避免出现javascript应用逻辑与垃圾器看到的不一致出现问题,等待垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿”(stop-the-world)。为了解决老生代回收时带来的停顿时间过长的问题,V8先从标记阶段入手,将原本要一口气回收的过程改为增量标记(incremental Marking),也就是拆分为许多小“步进”,每做完一“步进”就让javascript应用执行一会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。V8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。同时V8还引入了延迟清理与增量式整理,让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行清理,进一步利用多核性能降低每次停顿的时间。

查看垃圾回收日志

  • 查看垃圾回收日志的方式主要是在启动时添加–trace_gc参数。在进行垃圾回收时,将会从标准输出中打印垃圾回收的日志信息。
node --trace_gc -e "var a=[]; for(var i = 0; i < 10000000; i++) a.push(new Array(100));" > gc.log

通过分析垃圾回收日志,可以了解垃圾回收的运行状况,找出耗时阶段和原因。
通过在Node启动的时候使用–prof参数,可以得到V8执行时的性能分析数据,其中包含了垃圾回收执行时占用时间。

猜你喜欢

转载自blog.csdn.net/shadowfall/article/details/119797579