内存管理与垃圾回收 (GC)
在高级 Node.js 面试中,内存泄漏的排查和 V8 的内存管理机制是必考的底层知识。
V8 内存结构
Node.js 程序运行时的内存主要分为以下两部分:
V8 堆内存 (Heap)
- V8 引擎分配的内存,受限于 V8 的内存大小限制(64位系统默认约 1.4GB,32位系统默认约 0.7GB)。
- 所有的 JavaScript 对象、闭包等都存储在这里。
- 分为新生代 (New Space) 和 老生代 (Old Space)。
堆外内存 (Off-Heap / External Memory)
- 不受 V8 引擎内存大小限制的内存。
Buffer对象、C++ 层面分配的内存(如网络套接字、底层的 I/O 缓冲区)都属于堆外内存。- 这是 Node.js 处理大文件时(如视频切片流)避免内存溢出的关键。
垃圾回收机制 (Garbage Collection)
V8 采用分代式垃圾回收机制。
1. 新生代 (New Space)
- 特点:存放存活时间短的对象,内存空间较小(通常在 16MB ~ 32MB 左右)。
- 算法:采用
Scavenge算法(基于 Cheney 算法)。- 将新生代内存分为两半:
From空间和To空间。 - 垃圾回收时,检查
From空间中的存活对象,并将它们复制到To空间中,然后清空From空间。 - 最后,
From和To空间角色互换。
- 将新生代内存分为两半:
- 晋升机制:如果一个对象经历了多次 Scavenge 回收依然存活,或者
To空间占比超过 25%,该对象会被晋升到老生代。
2. 老生代 (Old Space)
- 特点:存放存活时间长或体积大的对象,占据了 V8 绝大部分内存。
- 算法:采用
Mark-Sweep(标记清除) 和Mark-Compact(标记整理)。- Mark-Sweep:遍历堆内存,标记所有存活的对象。然后清理掉没有被标记的对象。这会产生内存碎片。
- Mark-Compact:在空间不足以分配大对象时触发。将所有存活的对象往内存的一端移动,清理掉边界外的内存,解决内存碎片问题。
3. Orinoco 优化机制 (面试加分项)
为了避免垃圾回收时长时间暂停主线程(Stop-The-World),V8 引入了以下优化机制:
- 增量标记 (Incremental Marking):将标记过程分成小段,与 JS 主线程交替执行。
- 并发清理 (Concurrent Sweeping):在后台线程同时进行清理工作,不阻塞主线程。
Node.js 内存泄漏排查
内存泄漏是指本该被垃圾回收的对象,因为某些原因仍然被其他存活对象引用,导致无法被释放。
常见的内存泄漏场景
- 全局变量滥用:挂载在
global上的大对象(如未被清除的数组或 Map)。 - 闭包引起的内存泄漏:长生命周期的闭包中持有了巨大的外部作用域变量引用(比如在请求生命周期中定义了全局函数并持有
req/res的引用)。 - 缓存未设置上限:在内存中实现简单的 Map 缓存,但没有设置 LRU(最近最少使用)淘汰策略或最大容量,导致缓存无限增长。生产环境强烈建议使用 Redis。
- 事件监听器未移除:
eventEmitter.on()绑定了事件但没有在适当的时机removeListener(),导致监听器数组无限膨胀(Node.js 默认同一个事件超过 10 个监听器会抛出警告)。
高级排查工具与手段
process.memoryUsage()- 可以在应用中写一个定时器打印该方法的结果,观察
heapUsed(已使用的堆内存)是否随时间呈阶梯状持续上升,这是判断内存泄漏最基础的指标。
- 可以在应用中写一个定时器打印该方法的结果,观察
heapdump/v8模块打快照- 使用
v8.writeHeapSnapshot()生成内存快照文件(.heapsnapshot)。 - 通常需要打两个快照:一个在内存平稳时,一个在内存飙升时。
- 使用
- Chrome DevTools (Memory 面板)
- 将生成的
.heapsnapshot文件导入 Chrome 浏览器的 DevTools -> Memory 面板。 - 使用 Comparison(对比) 视图,对比两个快照,查看增加最多的对象类型,进而定位到泄露的具体代码位置(Retainers 视图可以查看是谁持有了它的引用)。
- 将生成的
- Clinic.js (性能诊断利器)
clinic doctor:生成直观的 CPU 和内存占用图表。clinic flame:生成火焰图,定位 CPU 密集型的 Long Task 阻塞。