当前位置: 首页 > 后端技术 > Node.js

V8内存管理和垃圾回收机制

时间:2023-04-03 17:17:17 Node.js

JavaScript引擎的内存空间主要分为栈和堆。栈是一个临时存储空间,主要存放局部变量和函数调用。基本类型的数据(Number、Boolean、String、Null、Undefined、Symbol、BigInt)存储在堆栈内存中。引用类型数据存放在堆内存中,引用数据类型的变量是对堆内存中实际对象的引用,存放在栈中。对于基本类型赋值,系统会在栈内存中为新变量分配一个新值,这个很容易理解。引用类型赋值,系统会在栈内存中为新变量赋值,这个值只是对同一个对象的引用,而原对象指向堆内存中的同一个对象。对于函数,解释器会创建一个“调用栈”来记录函数的调用过程。每次调用一个函数,解释器都可以将函数添加到调用栈中,解释器会为添加的函数创建一个栈帧(用于保存函数的局部变量和执行语句)并立即执行。如果正在执行的函数还调用了其他函数,新的函数会继续添加到调用栈中。函数执行后,对应的栈帧立即被销毁。查看调用栈的两种方式使用console.trace()")向Web控制台输出一个stacktrace。虽然浏览器开发者工具进行了断点调试,但是这个栈是非常轻量级的,用时创建,用后销毁,但不能无限增长,当分配的调用栈空间满了,会导致“栈溢出”错误。(functionfoo(){foo()})()为什么栈中存放的是基本数据类型,参考数据类型存储在堆中?JavaScript引擎在程序执行过程中需要使用栈来维护上下文的状态,如果栈空间很大,所有的数据都会存储在栈空间中,会影响效率上下文切换,影响整个程序的执行效率。堆空间中存放的数据比较复杂,大致可以分为以下五个区域:CodeSpace,MapSpace,LargeObjectSpace,NewSpace),oldgeneration(OldSpace),本文主要讨论新生代和老年代的内存回收算法。新生代的内存是临时分配的内存,生存期长。老年代的内存是常驻内存,存活时间长。新生代中使用Scavenge算法,回收新生代中的内存。所谓Scavenge算法将新生代空间一分为二,一半是对象区(from),另一半是空闲区(to)。新对象会先分配到from空间。当进行垃圾回收时,from空间中存活的对象会被复制到to空间中存放,非存活对象的空间会被回收。复制完成后,from空间和to空间互换,to空间变成新的from空间,原来的from空间变成to空间。该算法称为“Scavenge”。新生代的内存回收频率很高,速度也很快,但是空间利用率很低,因为有一半的内存空间处于“空闲”状态。老年代内存回收新生代中被多次回收且仍然存活的对象,会被转移到空间更大的老年代内存中。这种现象称为提升。以下两种情况,在垃圾回收过程中,如果发现某个对象之前已经被清理过,就会将其提升到老年代的内存空间。在from空间和to空间反转的过程中,如果to空间中的使用率已经超过25%,那么from中的对象直接提升到老年代的内存空间。因为老年代空间大,如果还用Scavenge算法频繁复制对象,性能开销就太高了。标记-清除(Mark-Sweep)老年代使用“标记-清除”来回收未存活的对象。分为标记和清除两个阶段。标记阶段遍历堆中的所有对象并对存活的对象进行标记,清除阶段清除未标记的对象。标记压缩(Mark-Compact)标记清除不会把内存一分为二,所以不会浪费空间。但是标记和清除后的内存空间会产生大量不连续的碎片空间。在这个不连续的碎片空间中,当遇到大对象时,可能会因为空间不足而无法存储。为了解决内存碎片问题,需要使用另一种算法——标记紧凑型(Mark-Compact)。标记并不会立即回收未存活的对象,而是将存活的对象移到一边,然后直接清除结束边界外的内存。增量标记为了避免JavaScript应用程序和垃圾收集器之间的不一致,在进行垃圾收集时,需要停止正在运行的程序,等待垃圾收集完成后再恢复程序的执行。这种现象被称为“完全停止”。如果需要恢复的数据太多,则全暂停时间会更长,影响其他程序的正常执行。为了防止垃圾回收时间过长影响其他程序的执行,V8将标记过程划分为小的子标记过程,同时让垃圾回收和JavaScript应用逻辑代码交替执行,直到标记阶段完成。我们称这个过程为增量标记算法。通俗的理解就是把垃圾回收这个大任务分成小任务,穿插在JavaScript任务中间。这个过程其实和ReactFiber的设计思路类似。参考V8引擎垃圾内存回收原理分析JavaScript中的V8引擎内存问题说说V8引擎中的垃圾回收机制《深入浅出Node.js》别人最近推出了100天的前端进阶计划,主要是深挖每个知识点背后的原理,欢迎关注微信公众号“木妈的星星”,一起学习,签到100天。同时也会分享一些自己的学习心得和想法,欢迎大家交流。