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

为NodeJS内存泄漏而烦恼?先从垃圾回收说起

时间:2023-04-03 18:36:00 Node.js

一般来说,内存管理有两种方式,一种是手动管理,一种是自动管理。手动管理需要开发者自己管理内存。什么时候申请内存空间,什么时候释放,需要慎重处理,否则很容易造成内存泄漏,指针乱飞。C语言开发是一个典型的需要手动内存管理的例子。自动管理通常通过垃圾回收机制来实现内存管理。NodeJS中的内存管理是自动管理的。垃圾收集垃圾收集器(garbagecollector,GC)通过判断对象是否还被其他对象引用来决定是否回收该对象的内存空间。垃圾回收前的内存在下图中,有些对象还在被其他对象使用,有些对象是完全隔离的,没有其他对象使用。这些完全隔离的对象可以被垃圾收集器回收。垃圾收集后的内存一旦垃圾收集开始运行,内存中那些完全孤立(无法访问)的对象将被删除,内存空间将被释放。垃圾收集的工作原理要了解垃圾收集的工作原理,您需要了解一些基本概念。基本概念常驻集大小(residentsetsize):NodeJS进程在运行时占用的内存大小,通常包括:代码、栈和堆。堆栈:包含原始类型数据和指向对象的引用数据。堆栈保存局部变量和指向堆上对象的指针或定义应用程序控制流(例如函数调用等)的指针。在下面的代码中,a和b都存储在堆栈中。functionadd(a,b){returna+b}add(4,5)Heap(堆):存放引用类型的数据,例如对象、字符串、闭包等。在下面的代码中,创建的Car对象将是保存在堆中。functionCar(opts){this.name=opts.name}constLightningMcQueen=newCar({name:'LightningMcQueen'})创建对象后,堆内存的状态如下:现在我们添加更多的对象:constSallyCarrera=newCar({name:'SallyCarrera'})constMater=newCar({name:'Mater'})堆内存状态如下:如果现在进行垃圾回收,不会释放任何内存,因为每个对象都在使用(可达)。现在我们修改代码如下:{name:'LightningMcQueen',power:900})letSallyCarrera=newCar({name:'SallyCarrera',power:500})letMater=newCar({name:'Mater',power:100})heap内存状态变为:如果我们不再使用Mater,通过Mater=undefined删除内存中对象的引用,则内存状态变为:此时,内存中的Mater不再被其他对象使用(unreachable),当垃圾回收运行时,Mater对象会被回收,其占用的内存会被释放。Shallowsizeofanobject:对象本身占用的内存大小。Retainedsizeofanobject:删除对象及其依赖对象后释放的内存大小大多数对象的生命周期都很短,而少数对象往往寿命更长。为了利用这种行为,V8将堆分为两部分,新生代和老年代。新生代中所有新的内存需求都在新生代中分配。新生代的大小很小,在1到8MB之间。年轻一代的内存分配非常便宜。V8会在内存中为对象一个一个分配空间。当到达年轻代的边界时,将触发垃圾回收。V8会在年轻代采用Scavenge回收策略。Scavenge使用复制进行垃圾收集。它将内存一分为二,每一部分空间称为半空间。两个空间中,只有一个在使用中,另一个闲置。使用中的半空间称为“Fromspace”,空闲的半空间称为“Tospace”。新生代的内存分配过程如下:对象从From空间分配,如果半空间已满,则执行Scavenge算法进行垃圾回收。检查From空间中的对象,如果对象可达,检查对象是否满足提升条件,如果满足,提升到老年代,否则将对象从From空间复制到To空间。如果对象不可达,则释放不可达对象的空间。复制后,翻转From空间和To空间。在年轻一代存活下来的对象被提升到老年代。老年代的对象有两个特点。一是存活对象多,二是存活时间长。如果在老年代使用Scavenge算法进行垃圾回收,复制存活对象的效率会很低,浪费了一半的空间。因此,在老年代,V8通常采用Mark-Sweep和Mark-Compact策略进行回收。Mark-Sweep即markremoval,主要分为mark和clear两个阶段。在标记阶段,会遍历堆中的所有对象,对存活的对象进行标记;在清理阶段,未标记对象的空间将被回收。与Scavenge策略不同,Mark-Sweep不会将内存一分为二,因此不会浪费空间。但是经过一次Mark-Sweep之后,内存空间就会变得不连续,这会给后续的内存分配带来问题。比如当需要分配一个比较大的对象时,没有一个分片支持分配,就会提前触发垃圾回收,虽然这个垃圾回收不是必须的。为了解决内存碎片问题,提高内存的利用率,引入了Mark-Compact(MarkCollat??ion)策略。Mark-Compact是对Mark-Sweep算法的改进。标记阶段与Mark-Sweep相同,只是对未标记对象的处理不同。而Mark-Sweep是立即回收未标记的对象,Mark-Compact是将存活的对象移到一边,然后清理结束边界外的内存。由于Mark-Compact需要移动对象,所以它的执行速度比Mark-Sweep慢。因此,V8主要使用Mark-Sweep算法,当空间内存分配不足时再使用Mark-Compact算法。常见的面试知识点、技术方案、教程,可以扫描二维码关注公众号“何里千寻”获取,也可以来这里https://everfind.github.io。