前言
之前文章的地址 学习node,本篇是讲 node 的内存控制。
V8 的垃圾回收机制与内存限制
了解这个,我们首先要对一些基本的概念有所了解,简单来说,内存里面分堆和栈,栈储存基本变量,堆储存复杂变量,栈是由编译器分配的,空间较小,堆是有操作系统分配的,内存较大,需要手动释放。
我们这里讲的内存限制,基本都是堆的,V8 的内存限制由 node 做的,有默认大小,可以进行调整,在启动时就确定了,不可动态添加。
V8 的垃圾回收机制,主要是将内存分为两种,新生代和老生代,采取不一样的垃圾回收算法。
新生代中都是存活时间较短的对象(前面有说过,内存都是讲的堆,堆里存储复杂变量),而老生代中都是存活时间较长或者常驻内存的对象。
新生代内存使用的 Scavenge 算法,它将空间一分为二,一个处于使用中,称为 From,一个处于闲置状态,称为 To。进行垃圾回收时,会检查 From 空间中存活的对象,然后赋值到 To 空间,接着释放 From 空间,之后将角色对调,即之前的 From 变成 To,To 变成 From。
以上是所有堆内存中,新生代内存的位置,也可以看到,总的内存大小就是新生代加老生代之和。
在新生代里,多次复制后依然存活,就会被移动到老生代中,也叫晋升。
晋升的条件有两个,一个是对象是否经历 Scavenge 回收,一个是 To 空间的内存占用比超过限制(超过 25%)。
老生代里,存活的对象会很多,复制的效率会变低,而且占用的空间也很大,为此,采用了标记清除和标记整理相结合的机制。
标记清除,分标记和清除两部分,标记活着的对象,然后清理没有被标记的对象,可以看出和 Scavenge 不同,这里是只处理没有标记的死亡的对象,下图即标记清除的示意图,黑色部分为清除后的死亡的对象。
可以看出,标记清除最大的问题是在进行清除后,内存空间会出现不连续的状态,这样,当出现一个大的对象,当前内存空间无法满足时,就会再触发垃圾回收。
为了解决这个问题,标记整理被提了出来,它和标记清除的区别在于,标记的过程中,把活的对象往一边迁移,然后,当移动完成后,直接清理,这时候得到的内存空间就是连续的,如下图所示
完成移动后,直接将右边的内存区域整个清除。
我们可以简单看看三个算法的对比
可以看到,标记整理速度是最慢的,所以在老生代里,主要使用标记清除,在空间不足时才使用标记整理。
我们很早就知道,js 的执行逻辑是单线程的,那么垃圾回收也是需要一个独立的线程来执行的,但是两个线程如果同时执行,那么同步就会是一个很大的问题,类似浏览器渲染进程里 ui 线程和 js 线程也不是同时执行而是互斥的,同一时刻只能有一方执行。
那么就会出现一些问题,假如垃圾回收线程执行的太久,那么就会影响 js 逻辑的执行,可能就会表现出卡顿,影响用户体验。
为此 V8 特意对垃圾回收过程进行了优化,例如标记阶段,可分成一小段一小段的标记,与 js 的逻辑交替执行,直到标记完成,这叫增量标记。
(看起来有点像 fiber, 或者说 fiber 有点像它)
同时还有延迟清理和增量整理,这样垃圾回收的过程更多的变成增量式的,同时还计划引入并行标记和并行清理,就是在垃圾回收执行的时候使用多核进行标记和清理。
高效使用内存
- 变量主动释放
- 手动回收闭包
内存构成
node 的内存构成主要有通过 V8 进行分配的部分和 node 自己分配的部分,受 V8 垃圾回收限制的主要是 V8 的堆内存。
内存泄露原因
缓存
大量的使用对象做缓存等
解决方法是开发者需要高效的使用内存
队列消费不及时
前端碰到的场景很少,后端碰到的比较多,很多时候,需要对并发做一个限制,当前并发达到峰值时,就把任务放进队列里等待,但是如果队列里存放的任务多了,也会造成性能问题、内存泄露。
主要有点,一是超时机制,当任务等待时间过长,就需要超时,而是拒绝拒绝机制,当队列拥塞时,新来的任务直接拒绝。
作用域未释放
也是开发者需要高效的使用内存,但是很多时候不一定能直接定位到原因,这时候可以借助一些第三方库来帮助排查内存泄露的具体位置。
大内存应用
如果需要操作大内存应用,可以使用 node 自己提供的 stream 模块来处理。
总结
通过这一章节,可以大致了解 V8 是怎么做垃圾回收的,那么不论是在开发高性能的 node 程序还是页面的 js 代码时都有一定的帮助。
而且这一章节还让我学习了后端在面对高并发时的一些处理逻辑,超时模式和拒绝模式应该是一个队列比较重要的机制。
还有一点,其实我们讲了这么多,都是介绍的 V8 的垃圾回收机制,前面有说过,node 的内存构成主要有通过 V8 进行分配的部分和 node 自己分配的部分,例如 node 的原生模块 Buffer,内存就不是由 V8 分配,而是由 node 自己分配的。
好了,就到这了~