关于Node.js的内存管理优化
内存管理一直是我们开发过程中备受关注的问题。每个程序都会占用计算机的一部分内存,因此我们必须了解分配和释放内存的原理。
Node.js凭借其高效的自动垃圾回收机制,让开发人员无需关心内存管理细节。虽然这对开发者很友好,但了解V8和Node.js中的内存管理机制仍然很重要,尤其是在处理大型应用程序时。
这篇文件讲一讲怎么合理地分配和释放内存,以及在Node.js中如何防止内存泄漏。
一、为什么高效的堆内存使用在Node.js中很重要?
在执行程序时,变量和对象存储在栈或堆中。ECMAScript规范本身并没有规定如何分配和管理内存,实现细节依赖于JavaScript引擎和底层系统架构。
堆是一大段连续的内存块,当数据存储在堆中后,除非被垃圾回收器删除,否则会一直占据内存。如果分配不当的话,内存就会被浪费,甚至引发内存泄漏。
V8采用分代回收
的垃圾回收管理方案,将堆内存分为新生代和老生代区域。新生代区域较小,老生代区域较大。
新生代区域通过Scavenge算法进行垃圾回收,将空间一分为二,一个叫From-Space
,一个叫To-Space
,From和To是相对的。分配对象时,会先分配到From-Space
。当垃圾回收时,检查From-Space
的存活对象,复制到To-Space
中。完成复制后,From-Space
和To-Space
对调。如果仅仅在这两个Space中复制移动,新生代区域也很快占满,需要将部分数据移入老生代区域,这个移动称为晋升
。对象晋升需要满足两个条件之一:1、对象之前经历过From-Space
到To-Space
的复制过程 2、To-Space
的占用率超过25%。
老生代区域采用Full Mark-Compact
方案进行垃圾回收,包括标记
、清除
、整理
三个阶段。通过判断可访问性标记出可达对象,将需要清理的对象所占用的内存空间添加到空闲空间列表中,在之后新对象分配内存时,可以从空闲空间列表找到大小合适的空间直接利用。当内存占用高度分散时,会整理老生代区域,即复制存活对象到一段连续的空间。因为要回收的对象在老生代区域只是少部分,清理和整理的频率实际上很低。
二、导致Node.js内存泄漏的原因
垃圾收集器会查找不再使用的对象并释放内存,但有时它可能无法跟踪每一个对象引用,尤其是对于大型应用程序,这就导致了内存泄漏的发生。
内存泄漏最常见的原因有以下几种:
- 多处引用
- 全局变量
- 闭包
- 定时器
- 事件
三、如何定位Node.js内存泄漏的问题
有几种工具可用于检测和调试Node.js中的内存泄漏,包括Chrome DevTools、Node的process.memoryUsage API等。
Chrome DevTools
如果你的Node.js程序入口是./src/main.js
,先执行node --inspect ./src/main.js
调试程序,再打开Chrome浏览器的chrome://inspect
,可以看到以下界面:

点击inspect
,选择内存
tab的拍摄堆快照:

使用快照功能可以检查变量和它们的Retained Size
大小,也可以使用多个快照,比如在内存泄漏之前拍摄快照,在内存泄漏之后拍摄另一个快照,然后将两者进行比较。

界面上有两项数据,Shadow Size
(浅层大小)和Retained Size
(保留的大小)。Shadow Size
指的是对象本地的大小,除了数组和字符串之外,大多数对象的浅层大小都不会太大。Retained Size
指的是对象所引用内存的大小,回收对象会将引用的内存也一并回收,所以指代的是回收内存后会释放出来的内存大小。
使用Chrome DevTools不是生成快照的唯一办法,还可以在Node程序运行时打点触发。在NodeJS 11.13之前,使用heapdump,在11.13之后,使用
1 | require("v8").writeHeapSnapshot(); |
生成快照后同样导入到Chrome DevTools中分析。
更多内存快照的使用方法可参考NodeJS官方文档。
process.memoryUsage API
还可以使用Node的process.memoryUsage API查看内存使用情况。程序中运行process.memoryUsage()
,返回以下指标:
rss
- 分配的内存量heapTotal
- 已分配堆的总大小heapUsed
- 执行进程时使用的内存总量arrayBuffers
- 为Buffer实例分配的内存
四、垃圾回收机制
垃圾回收器负责释放内存,为了有效地工作,垃圾回收算法必须正确定义和识别可以释放内存的情况。
在引用计数
垃圾回收算法中,如果堆中的对象在堆栈中不再有引用,即引用计数为0,则该对象将被垃圾回收。尽管此算法在大多数情况下都有效,但不适用于循环引用的情况下。
Node.js不采用引用计数
的算法,而是通过判断能否从根对象
直接或间接访问目标对象,来决定是否需要回收。在浏览器中,根对象是window
;在Node.js中,根对象是global
。这个算法叫作标记扫除
,解决了循环引用的问题。开发者可以显式声明对象无法从根对象
访问,以确保目标对象被垃圾回收。
五、如何避免内存泄漏
1、避免使用全局变量
全局变量包括使用var
关键字、this
关键字声明的变量和未使用关键字声明的变量。它们始终可以从根访问,因此除非明确设置为null
,否则无法进行垃圾回收。
1 | function variables() { |
以上代码3个变量都是全局变量,为了避免这种情况,应该采用严格模式来写代码。
2、使用JSON.parse
JSON的语法比JavaScript简单得多,因此它比JavaScript对象更容易解析。
事实上,如果您使用大型JavaScript对象,通过将字符串化形式解析为JSON,可以在V8和Chrome中将性能提高1.7倍。
在其它JavaScript引擎如 Safari中,性能可能会更高。这种优化方法在Webpack中用于提高前端应用程序的性能。
1 | const obj = { name: 'pomelo' }; |
改写为以下形式性能更好:
1 | const obj = JSON.parse('{"name":"pomelo"}'); |
3、大数据分批处理
在处理大数据,通常是Excel文件导出时,往往会遇到内存不足的问题。比增加机器内存更好的办法,是将数据分块,分批导出。
4、注意定时器的使用
确保定时器能在生命周期中得到清除,在恰当的时机调用clearTimeout
,clearImmediate
,clearInterval
是个好习惯。
5、及时移除事件监听器
当添加事件监听器后,要记得在不使用时移除,同时防止多次注册同一监听器,还要限制同个事件的订阅器个数。
1 | const obj = { |
6、释放闭包中无用的变量
如果一个变量在一个函数中使用,它会在函数返回时被标记为垃圾收集——但对于闭包来说可能不是这种情况。
1 | const func = () => { |
上面例子中,obj1因为在闭包中,即使程序执行完,也不会被垃圾回收,可以手动声明释放:
1 | const func = () => { |