node中的v8内存管理和垃圾回收机制
前言
一般使用js在前端开发中,并不是很关注GC,但是node几大的拓展了js的应用常见,当应用场景移到服务端之后,我们就能发现内存管理的好坏,垃圾回收的优良,都会对服务构成影响。在node中,这一切都与node的js引擎v8息息相关。
一、v8的内存限制:
在一般的后端开发语言中,在基本内存使用上没有限制,在Node中过JavaScript 使用内内存时就会发现只能使用部分内存(64系统下为1.4 GB,32系统下为0.7 GB)。
这个问题的要因在于Node基于V8构建,所以在Node中使用的JavaScript对象基本上都是通过V8自己的方式通过分配和管理的。
要知晓V8为何限制了内存的用量,则需要回归到V8在内存上的使用策略。
$ node
> process.memoryUsage();
{ rss: 23523328,
heapTotal: 9682944,
heapUsed: 5800264,
external: 8766 }
node可以在启动时,调整内存限制的大小:
node --max-old-space-size=1700 test.js
node --max-new-space-size=1024 test.js
二、v8的垃圾回收机制
v8的垃圾回收策略主要基于分代式垃圾回收机制。
在垃圾回收的演变过程中,没有任何一种垃圾回收算法能够胜任所有场景,故此,现代的垃圾回收算法中按照对象的存活时间将内存的垃圾回收进行不同分代,然后分别对不同分代的内存使用最适合的算法。
1、v8的内存分代:
在v8中,主要将内存分为新生代和老生代,新生代中的对象存货时间较短的对象,老生代中的对象为存活时间较长或常驻的对象。
v8堆的整体大小就是新生代所有内存空间加上老生代的内存空间。
在64位系统和32位系统下分别可以使用约1.4GB和0.7GB的大小。
2、新生代算法
新生代中对象主要通过Scavenge算法进行垃圾回收。在其中主要又采用了Cheney算法。
Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,这两个空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的空间称为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。
简而言之,在垃圾回收的过程中,就是通过将存活对象在两个空间之间进行复制。
Scavenge的缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异的表现。
由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模地应用到所有的垃圾回收中。但可以发现,Scavenge非常适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰适合这个算法。
实际使用的堆内存是新生代中的两个空间大小和老生代所用内存大小之和。
当一个对象经过多次复制依然存在时,它将会被认为是生命周期较长的对象,这种对象会被移到老生代中,采用新的算法进行管理,这种移动称之为“晋级”。
对象晋级的条件主要有两个,一个是对象是否经历过Scavenge回收,另一个是To空间的内存占用比超过限制。
第二个原因的25%限制阈值是因为完成回收后,To空间和From空间会进行调换,To =》 From,如果To的占比过高,变成From后将影响后续的内存分配。
3、老生代算法
对于老生代中的对象,由于存活对象占较大比重,再采用Scavenge的方式会有两个问题:一个是存活对象较多,复制存活对象的效率将会很低;另一个问题依然是浪费一半空间的问题。这两个问题导致应对生命周期较长的对象时Scavenge会显得捉襟见肘。为此,V8在老生代中主要采 用了 Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收。
Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。
与Scavenge相比,Mark-Sweep 并不将内存空间划分为两半,所以不存在浪费一半空间的行为。与Scavenge复制活着的对象不同,Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。
可以看出,Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象。
活对象在新生代中只占较小部分,死对象在老生代中只占较小部分,这是两种回收方式能高效处理的原因。
(黑色为标记死亡对象)
Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。
这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。为了解决Mark-Sweep的内存碎片问题,Mark-Compact被提出来。
Mark-Compact是标记整理的意思,是在Mark-Sweep的基础上演变而来的。
它们的差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。
(白色格子为存活对象,深色格子为死亡对象,浅色格子为存活对象移动后留下的位置)
4、Incremental Marking
为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿”(stop-the-world)。
在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。但V8的老 生代通常配置得较大,且存活对象较多,全堆垃圾回收(fUll垃圾回收)的标记、清理、整理等 动作造成的停顿就会比较可怕,需要设法改善。
为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段人手,将原本要一口气停顿完成的动作改为增量标记(incrementalmarking),也就是拆分为许多小“步进”,每做完一“步进” 就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。
V8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。
V8后续还引入了延迟清理(lazy sweeping )与增量式整理(incremental compaction ),让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行清理,进一步利用多核性能降低每次停顿的时间。
三、小结
从v8的自动垃圾回收机制的设计角度可以看到,v8对内存使用进行限制的缘由。
新生代设计为一个较小的内存空间是合理的,而老生代空间过大对于垃圾回收并无特别意义。V8对内存 限制的设置对于Chrome浏览器这种每个选项卡页面使用一个V8实例而言,内存的使用是绰缚有余了。对于Node编写的服务器端来说,内存限制也并不影响正常场景下的使用。但是对于V8的垃圾回收特点和JavaScript在单线程上的执行情况,垃圾回收是影响性能的因素之一。想要高性能的执行效率,需要注意让垃圾回收尽量少地进行,尤其是全堆垃圾回收。
以Web服务器中的会话实现为例,一般通过内存来存储,但在访问量大的时候会导致老生代中的存活对象骤增,不仅造成清理/整理过程费时,还会造成内存紧张,甚至溢出。
PS:感谢大神的肩膀
https://www.ituring.com.cn/book/1290