提取heap snapshot
在 Node.js 的本地运行环境中,开发者可以利用多种工具来提取堆(heap)转储,以分析内存使用情况,检测内存泄漏,优化性能。以下是一些常用且有效的工具:
heapdump 模块
heapdump 是一个流行的 Node.js 模块,允许在运行时生成 V8 引擎的堆快照。通过这些快照,开发者可以深入分析应用程序的内存使用情况,识别潜在的内存泄漏。
安装与使用:
1 | npm install heapdump |
在应用程序中引入并使用:
1 | const heapdump = require('heapdump'); |
生成的 .heapsnapshot 文件可以在 Chrome DevTools 的 Memory 面板中加载和分析。
注意事项:
- 生成堆快照是同步操作,可能会导致主线程暂停,尤其是在堆内存较大时。
- 在生产环境中使用时,应谨慎操作,避免对服务造成影响。
v8 模块的 writeHeapSnapshot 方法
自 Node.js v11.13.0 起,v8 模块提供了 writeHeapSnapshot 方法,允许直接生成堆快照,无需额外的第三方模块。
使用示例:
1 | const v8 = require('v8'); |
优点:
- 无需安装额外模块,减少了依赖。
- 提供了与
heapdump类似的功能,适用于需要内置解决方案的场景。
使用 Chrome DevTools 的内存分析工具
在本地开发环境中,开发者可以利用 Chrome DevTools 的内存分析工具,直接连接到运行中的 Node.js 进程,生成和分析堆快照。
步骤:
启动 Node.js 应用时,添加
--inspect标志:1
node --inspect your_app.js
在 Chrome 浏览器中,访问
chrome://inspect,找到对应的 Node.js 进程,点击inspect。在打开的 DevTools 窗口中,导航到
Memory面板,选择Heap snapshot,然后点击Take snapshot。
优点:
- 提供了直观的图形界面,便于分析。
- 无需在代码中添加额外的逻辑。
注意事项:
- 适用于本地开发和调试环境。
- 在生产环境中使用时,可能需要考虑安全性和性能影响。
使用 --heapsnapshot-signal 标志
自 Node.js v12.0.0 起,提供了 --heapsnapshot-signal 标志,允许在接收到特定信号时生成堆快照。
使用示例:
1 | node --heapsnapshot-signal=SIGUSR2 your_app.js |
然后,在运行时发送 SIGUSR2 信号:
1 | kill -USR2 <pid> |
优点:
- 无需修改应用代码。
- 适用于需要在特定时刻生成堆快照的场景。
注意事项:
- 需要确保发送信号的权限。
- 在生产环境中使用时,应评估对服务的影响。
node-oom-heapdump 模块
node-oom-heapdump 是一个用于在发生内存溢出(Out of Memory)时自动生成堆快照的模块,帮助开发者分析导致内存溢出的原因。
安装与使用:
1 | npm install node-oom-heapdump |
在应用程序中引入并配置:
1 | require('node-oom-heapdump')({ |
优点:
- 自动捕获内存溢出时的堆快照,便于事后分析。
- 提供了与
heapdump类似的功能,但专注于内存溢出场景。
注意事项:
- 适用于需要监控内存溢出的应用程序。
- 在生产环境中使用时,应确保存储空间充足,以保存生成的堆快照。
heapdump-analyser 工具
heapdump-analyser 是一个用于分析堆快照的命令行工具,帮助开发者查找内存泄漏和分析内存使用情况。
安装与使用:
1 | npm install -g heapdump-analyser |
使用示例:
1 | heapdump-analyser dump.heapsnapshot |
优点:
- 提供了命令行界面,便于在终端中分析堆快照。
- 支持查找特定的类或闭包,帮助定位内存泄漏。
注意事项:
- 需要与其他工具配合使用,如
heapdump或v8.writeHeapSnapshot,以生成堆快照。 - 在分析大型堆快照时,可能需要较长时间。
使用 --heapsnapshot-near-heap-limit 标志
自 Node.js v14.18.0 起,提供了 --heapsnapshot-near-heap-limit 标志,允许在接近堆内存限制时自动生成堆快照。
使用示例:
1 | node --max_old_space_size=500 --heapsnapshot-near-heap-limit=1 your_app.js |
优点:
- 自动捕获接近内存限制时的堆快照,便于分析内存使用情况。
- 无需修改应用代码,减少了维护成本。
注意事项:
- 需要根据应用的内存使用情况,合理设置
--max_old_space_size和--heapsnapshot-near-heap-limit的值。 - 在生产环境中使用时,应评估对服务的影响。
总结
在 Node.js 的本地运行环境中,开发者可以根据具体需求和场景,选择合适的工具来提取和分析堆快照。无论是使用内置的 v8.writeHeapSnapshot 方法,还是第三方模块如 heapdump,都可以帮助深入了解应用的内存使用情况,检测内存泄漏,优化性能
分析快照
借助chrome devtool
浏览器开发者工具 -> Memory -> 上传快照
Memlab
https://www.npmjs.com/package/memlab
1 | npm install -g memlab |
Memlab是什么
Memlab is a memory testing framework for JavaScript。
Analyzes JavaScript heap and finds memory leaks in browser and node.js。
Memlab是一个JavaScript内存测试框架,可用于在浏览器、Node环境中分析JavaScript堆内存并检测内存泄露。它通过自定义测试场景,与SPA应用交互(使用 Puppeteer API),然后自动完成内存泄漏检查。
它的工作原理如下:
- 与浏览器交互并获取
JavaScript堆快照 - 分析堆快照并识别内存泄漏
- 对内存泄漏进行聚合、分组
- 生成可Debug的分析结果
Memlab的特点
- 面向对象的堆遍历 API:支持自定义内存泄露检测器,支持基于Chromium内核的应用(浏览器、Node环境、Electron、Hermes)
- Memory CLI 工具箱:内置 CLI 工具箱和 API
- Node环境下支持内存断言:可以对单元测试或运行中的Node应用保存堆快照,执行内存检查和内存断言
如何安装
1 | npm install -g memlab |
- 运行环境要求:Node.js 16+
- 需要科学上网:Memlab内部依赖种包含Puppeteer,正常情况下安装会报错。
- 针对第2点,官方建议设置环境变量
PUPPETEER_SKIP_DOWNLOAD先忽略浏览器的下载。手动安装好Puppeteer后,再手动下载Chromium文件,解压并放到Puppeteer的默认读取目录下。
如何使用
MemLab是在基于Chromium内核的浏览器中,运行预定义的测试场景并对 JavaScript heap snapshots 进行差异分析,从而发现内存泄漏,步骤如下:
- 导航到目标页面并返回
- 查找未释放的对象
- 显示泄露追踪结果
创建一个测试场景
将该测试场景保存为/memlab/scenario.js
1 | // 测试场景的初始url |
运行测试场景
1 | memlab run --scenario /memlab/scenario.js |
运行结果分析

第一部分,MemLab 会实时生成一个面包屑,显示与目标网页交互的进度,对每个步骤的解读如下:
page-load[6.5MB](baseline)[s1]- 测试试场景起点
- 初始页面加载时,JavaScript 堆内存大小为 6.5MB。
- baseline内存快照将作为 s1.heapsnapshot 保存在磁盘上。
action-on-page[6.6MB](target)[s2]- 执行交互操作
- 内存大小增加到 6.6MB
- target内存快照将作为 s2.heapsnapshot 保存在磁盘上。
revert[7MB](final)[s3]- 执行回退/反向操作
- 网页内存达到7MB,
- final内存快照将作为 s3.heapsnapshot 保存在磁盘上。
第二部分,对检测到的内存泄露进行汇总
- 内存泄露节点数
- 泄露内存占用大小
第三部分,按照内存泄露类型的相似性,对每个种类提取一个代表性的内存泄露节点进行展示,图中是创建1024个分离的DOM节点的内存泄露检测结果分析。
1 | window.leakedObjects = []; |
map这是被访问对象的 V8 HiddenClass(V8 在内部使用它来存储对象结构信息和对其原型的引用) - 在大多数情况下,这是 V8 实现的细节,可以忽略。prototypewindows实例leakedObjects表明leakedObjects是Window的属性,大小为148.5KB,指向Array对象0分离的 HTMLDIVElement元素,被存储为 leakedObjects 数组的第一个元素(Memlab 只打印一个具有代表性的内存泄露)
1 | [window](object) -> leakedObjects(property) -> [Array](object) |
扩展应用
检测未释放的超大对象
1 | // 未清空EventListener,无法释放eventHandler函数,eventHandler函数保存了对bigArray的引用 |
上述示例在运行测试场景时,无法检测到内存泄露。因为Memlab的泄漏检测器仅将满足以下所有条件的对象视为内存泄漏:
- 对象在触发action时分配 内存
- 在触发back后,对象 内存 没有被释放
- 该对象是一个分离的 DOM 元素或一个未挂载的 React Fiber 节点
这种情况下,可以用LeakFilter来自定义规则过滤内存泄露对象(如内存占用大小),LeakFilter会对每一个由action触发内存分配,但在back后未释放内存的对象进行调用。
1 | // ... |
另外也可以创建一个单独的leakFilter.js文件
1 | function leakFilter(node, _snapshot, _leakedNodeIds) { |
检测所有的内存泄露
默认情况下,Memlab只报告准确度高的内存泄漏(由其内置的内存泄漏检测器进行判断)。可能会存在一些内存泄漏,Memlab不会报告。
使用如下命令可以检测所有的 内存 泄露(确保测试场景不包含LeakFilter):
1 | memlab run --scenario /memlab/scenarios.js |
直接分析内存快照
通常情况下,Memlab的内存分析数据来源于Memlab对Puppeteer API的调用。
通过Memlab内置的Memlab API,可以使用Memlab直接分析基于从Chrome或任何基于Chromium内核的应用中获取的单个JavaScript堆快照,检测内存问题
1 | $ memlab view-heap --snapshot <PATH TO .heapsnapshot FILE> |
自动化内存泄露检测
Memlab支持自动化内存泄露检测,配置步骤如下:
- 准备覆盖关键交互的测试场景
- 通过
Memlab CLI或Memlab API触发测试场景运行 - 收集结果
在CLI中运行
1 | $ memlab run --scenario /path/to/test/scenario/file.js \ |
在Node.js中运行
1 | const {run} = require('@memlab/api'); |
Memlab 运行完成后,所有结果和数据将保存在指定的工作目录中(workDir),可以使用内置的结果分析器BrowserInteractionResultReader对结果进行读取并输出。
1 | const {BrowserInteractionResultReader} = require('@memlab/api'); |
Node.js 进程中常见的泄漏路径锚点
常见的泄漏路径锚点:
(global property): 全局变量或模块级的变量。(closure): 闭包,通常是事件监听器或定时器(setTimeout,setInterval)中的引用。(array)或(map): 可能是未清理的缓存结构。(native): 在 Node.js 场景中,这意味着 C++ 或Buffer引用的堆外内存。你需要向上追踪,找到持有这个Native块引用的 JavaScript 对象(通常是Buffer或ArrayBuffer实例)。
全局变量 (Global Variables)
任何直接或间接挂载在全局对象(global 或 process)上的对象,除非显式设为 null,否则永远不会被垃圾回收。
| 锚点类型 | 引用路径中的表现 | 常见代码模式 |
|---|---|---|
Global | (GC Root) → (global property) → LeakyCache | 不安全的缓存或单例: 将大型缓存对象、日志对象或配置对象直接挂在 global.cache 或 process.leaks 上。 |
Module | (GC Root) → (system / Context) → (module) → privateData | 模块级作用域的缓存: 在文件顶层作用域(Module Scope)声明了一个对象,并在应用生命周期内不断向其中添加数据,但从未清理。 |
Require Cache | (GC Root) → (module) → exports → LargeArray | 模块导出错误: 模块导出的对象被全局引用,而该对象又在不断增长。 |
泄露根源:定时器和事件(Timeouts & Event Emitters)
这是最常见的泄漏类型之一。未清除的定时器或事件监听器会持有其回调函数(Closure),进而持有该回调函数作用域内的所有变量,使其无法被回收。
| 锚点类型 | 引用路径中的表现 | 常见代码模式 |
|---|---|---|
Timeout / Immediate | (GC Root) → TimersList → Timeout → Closure → LeakedObject | 未清除的定时器: 忘记调用 clearInterval() 或 clearTimeout()。即使定时器只执行一次,如果它引用的对象很大,也会造成短暂泄漏。 |
Listener | (GC Root) → EventEmitter → Listener → Closure → LeakedObject | 未移除的事件监听器: 在对象销毁时,忘记调用 emitter.removeListener() 或 emitter.off()。常见的场景是请求结束时未清理的 Socket 事件或自定义事件。 |
泄露根源:HTTP 请求和 Context 泄漏
在 Koa/Express 等框架中,最危险的是请求级变量被意外地提升到全局作用域或闭包中。
| 锚点类型 | 引用路径中的表现 | 常见代码模式 |
|---|---|---|
Koa Context | (GC Root) → Closure → KoaMiddleware → RequestObject | 未销毁的请求上下文: 中间件中的闭包意外捕获了某个请求的 ctx 或 req 对象,导致请求结束后本应回收的对象被保留。 |
Promise / AsyncHook | (GC Root) → Promise → Closure → HeavyData | 未完成的 Promise 链: Promise 链条没有正确关闭或抛出错误,导致 Promise 内部状态和其捕获的变量长时间存在。 |
泄露根源:堆外内存(Buffer/Native)
这是你当前遇到的问题,其核心在于 JavaScript 对象阻止了底层 C++ 内存的释放。
| 锚点类型 | 引用路径中的表现 | 常见代码模式 |
|---|---|---|
Buffer / Native | JS Object → Buffer→ Native | Buffer 缓存未清理: 使用 Buffer 或 ArrayBuffer 存储大量数据(例如文件内容、加密结果),但该 Buffer 对象被一个未清理的缓存 Map 或全局对象引用。 |
Native Hook | (GC Root) → C++ Addon → Native Data → Buffer | C++ 插件错误: 使用了 Node.js 原生模块(如数据库驱动、图像处理库),但 C++ 代码没有正确释放底层内存,或者 C++ 对象被 JavaScript 对象错误地引用。 |