☰
memlab CollectionsHoldingStaleAnalysis 详解:定位 Map/Set/Array 中滞留 Detached 对象的内存泄漏源
2026/10/13 18:05:17 网站建设 项目流程
  • 开发工具
  • 测试
  • CLI
  • MCP 服务

【免费下载链接】memlab

A framework for finding JavaScript memory leaks and analyzing heap snapshots

项目地址:https://gitcode.com/gh_mirrors/me/memlab
点击查看免费下载

本篇围绕 memlab 内置堆分析插件CollectionsHoldingStaleAnalysis(CLI 命令名collections-with-stale)展开:它用于在单个堆快照中找出仍持有“陈旧对象”(detached DOM 节点、未卸载的 React Fiber 节点)的Map、Set、Array集合。读完本文,你可以直接用memlab analyze collections-with-stale或@memlab/heap-analysis的编程 API 定位“集合把已卸载/已移除对象一直拽在堆里”这类典型泄漏,并理解其底层扫描逻辑、stale 判定规则与输出格式。

什么是“持有陈旧对象的集合”

前端应用中很常见的泄漏模式是:组件卸载或 DOM 节点被移除后,本应可被垃圾回收的对象仍被某个长生命周期的集合(缓存、监听器注册表、索引表等)强引用着。这类集合里的元素可以被称为“陈旧对象”(stale objects)。在堆快照的视角下,这些陈旧对象有两类典型特征:

  • Detached DOM 节点:已从文档树移除、但仍未被回收的 DOM 元素(例如Detached HTMLDivElement);
  • 未卸载的 Fiber 节点(Detached FiberNode):React 组件卸载后残留在堆中的 Fiber 对象。

CollectionsHoldingStaleAnalysis就是专门针对这两种情况做的单快照分析:它扫描堆中的Map、Set、Array,统计每个集合里有多少直接子元素处于 detached 状态,并按陈旧元素占用的 retained size 排序输出,帮助开发者快速锁定“哪个集合、多大、里面有哪些陈旧元素”。

类结构与继承关系

根据官方 API 文档 CollectionsHoldingStaleAnalysis,该类的定义与成员如下:

成员说明
构造函数new CollectionsHoldingStaleAnalysis(),无参数
继承自BaseAnalysis(进而继承内部Analysis抽象类)
getCommandName()覆写基类方法,返回 CLI 命令名collections-with-stale,用于memlab analyze <ANALYSIS_NAME>引用
analyzeSnapshotFromFile(file, options?)继承自基类,对单个.heapsnapshot文件运行分析,返回Promise<AnalyzeSnapshotResult>

源码位于 CollectionsHoldingStaleAnalysis.ts,并经由 heap-analysis 包入口 导出:

// packages/heap-analysis/src/plugins/CollectionsHoldingStaleAnalysis.ts export default class CollectionsHoldingStaleAnalysis extends BaseAnalysis { public getCommandName(): string { return 'collections-with-stale'; } /** @internal */ public getDescription(): string { return 'Analyze collections holding stale objects'; } // ... }

getCommandName()的返回值同时决定了 CLI 侧的调用方式,getDescription()则会显示在memlab analyze list的输出中。

CLI 使用方式

在 memlab 命令行工具中,该分析对应memlab analyze的collections-with-stale子命令(见 CLI-commands.md):

memlab analyze collections-with-stale

它接受的插件级参数有两个(源码中由getOptions()返回SnapshotFileOption与OutputOption):

参数说明
--snapshot指定待分析堆快照文件的路径(.heapsnapshot)
--output分析结果输出格式,可选text/json,默认text

此外还支持memlab analyze的全局选项,例如--work-dir设置本次分析的工作目录、--verbose显示更多细节、--silent静音终端输出等。一个典型用法是:

# 对指定快照运行集合陈旧对象分析,输出 JSON 便于脚本处理 memlab analyze collections-with-stale \ --snapshot=./memlab-logs/tab-0/2026-10-12-14-00-00.heapsnapshot \ --output=json

适用前提:该分析面向单个堆快照。它不支持目录级多快照分析——在 CollectionsHoldingStaleAnalysis.ts 中,analyzeSnapshotsInDirectory()被显式覆写为抛出异常(CollectionsHoldingStaleAnalysis does not support analyzeSnapshotsInDirectory)。如果需要观察对象随时间增长的趋势,应改用unbound-collection、unbound-object等多快照分析插件。

编程 API:analyzeSnapshotFromFile()

按照 API 文档,可以通过继承自BaseAnalysis的analyzeSnapshotFromFile()以编程方式运行分析:

import {CollectionsHoldingStaleAnalysis} from '@memlab/heap-analysis'; const analysis = new CollectionsHoldingStaleAnalysis(); // 分析控制台的输出被保存到 result.analysisOutputFile const result = await analysis.analyzeSnapshotFromFile(snapshotFile);

参数与返回值(定义见 BaseAnalysis.ts):

  • file: string——.heapsnapshot文件的绝对路径;
  • options?: RunHeapAnalysisOptions—— 可选的运行配置,当前仅支持workDir:指定后,分析过程的中间文件、日志文件与最终输出都会写入该目录,目录不存在时 memlab 会自动创建;
  • 返回值Promise<AnalyzeSnapshotResult>—— 其中analysisOutputFile字段是本次分析控制台输出的日志文件路径(类型定义见 PluginUtils.ts)。

指定工作目录的完整示例:

const analysis = new CollectionsHoldingStaleAnalysis(); const result = await analysis.analyzeSnapshotFromFile(snapshotFile, { // 若目录不存在,memlab 会自动创建 workDir: '/tmp/your/work/dir', });

内部实现上,analyzeSnapshotFromFile()会先注册一个日志文件,然后以snapshot: file的伪 CLI 参数形式调用插件的process(),最后把日志文件路径作为AnalyzeSnapshotResult返回。分析结果本身(各集合的陈旧元素列表)则以终端文本或 JSON 形式写入该日志文件。

源码级剖析:扫描与 stale 判定逻辑

只关注三类集合:Map、Set、Array

分析的核心在process()回调(memlab analyze collections-with-stale的执行入口,见 源码):先通过pluginUtils.loadHeapSnapshot(options)加载并解析堆快照(同时计算 dominator 树与 retained size),再调用getCollectionsWithStaleValues()收集结果,最后按--output配置选择print()(文本)或printJson()(JSON)输出。

哪些节点被视为“集合”?由实例成员staleCollectionMapper决定:

// packages/heap-analysis/src/plugins/CollectionsHoldingStaleAnalysis.ts private staleCollectionMapper: Map<string, (node: IHeapNode) => CollectionStat> = new Map([ ['Map', processMapOrSet], ['Set', processMapOrSet], ['Array', processCollectionChildren], ]);

getCollectionsWithStaleValues()遍历快照中所有可追踪节点(snapshot.nodes.forEachTraceable),仅对名称命中Map/Set/Array的节点收集统计;统计中没有任何陈旧子元素的集合会被直接丢弃(staleChildren.length > 0才入列)。从源码结构看,这一设计是刻意为之:WeakMap/WeakSet持有弱引用,不构成强引用泄漏,因此不在分析范围内。

Map/Set 需要透过 table 边找到真正的元素

Map与Set在 V8 堆快照中有一层间接结构:集合对象本身通过一条名为table的边指向内部的哈希表对象,元素是挂在该表对象上的。processMapOrSet()正是利用了这一点:

function processMapOrSet(node: IHeapNode): CollectionStat { // 取集合对象的 table 出边,指向内部哈希表 const tableEdge = utils.getEdgeByNameAndType(node, 'table'); if (!tableEdge) { return initCollectionStat(node); } return processCollectionChildren(tableEdge.toNode); }

而Array的元素直接通过下标边挂在数组节点上,所以直接交给processCollectionChildren()。getEdgeByNameAndType的实现见 Utils.ts。

每个集合的统计项:CollectionStat

对每个命中的集合,分析会构建一个CollectionStat统计结构:

type CollectionStat = { collection: IHeapNode; // 集合节点本身 staleChildren: IHeapNode[]; // 处于 stale 状态的直接子元素 childrenSize: number; // 集合的直接子元素总数 staleRetainedSize: number; // stale 子元素的 retainedSize 累加值 };

processCollectionChildren()的逻辑是逐条遍历集合节点的出边(node.references),对每个目标元素节点做 stale 判定并累计:

function processCollectionChildren(node: IHeapNode): CollectionStat { const stat = initCollectionStat(node); const refs = node.references; for (const ref of refs) { ++stat.childrenSize; const elementNode = ref.toNode; if ( !utils.isDetachedDOMNode(elementNode) && !utils.isDetachedFiberNode(elementNode) ) { continue; } stat.staleChildren.push(elementNode); stat.staleRetainedSize += elementNode.retainedSize; } return stat; }

这里的关键是只检查集合的直接子元素,而不是集合可支配(dominated)的整个子树——这与“集合直接持有元素”的语义一致,也避免把嵌套在普通对象内部的 detached 节点误计入。

stale 判定:isDetachedDOMNode 与 isDetachedFiberNode

两个判定函数都来自@memlab/core的 utils。其共同基础是isDetached():

// packages/core/src/lib/Utils.ts function isDetached(node: IHeapNode): boolean { if (config.snapshotHasDetachedness) { return node.is_detached; // 快照带 detachedness 元数据时直接读字段 } return node.name.startsWith('Detached '); // 否则退回按节点名称前缀判断 }
  • isDetachedFiberNode(node):node是FiberNode/Detached FiberNode且满足isDetached;
  • isDetachedDOMNode(node):排除 Fiber 节点后,判断节点是否处于 detached 状态(例如Detached HTMLDivElement、Detached InternalNode等已从文档树摘除的 DOM 节点)。

需要留意的适用边界:源码注释明确指出,当快照未携带 detachedness 字段时,判定依赖Detached名称前缀,这类 FiberNode 若无 detachedness 字段则无法被识别(isDetachedFiberNode上方的 NOTE 注释)。

输出格式:文本与 JSON

结果统一按staleRetainedSize降序排序,且只输出前MAX_COLLECTION_STAT_ITEMS = 20个集合(常量定义在 源码第 21 行)。

文本模式(默认,print()):若没有任何命中集合,输出Congratulations! No collections holding stale objects;否则逐条打印:

Collections holding stale objects: · Array (@12345) 2.4 MB (3 out of 102 elements are stale): @12900, @12901, @13055, ...

每条包含集合类型名、节点 ID、集合的 retained size(经utils.getReadableBytes人类可读化),以及“N out of M elements are stale”的陈旧比例描述;陈旧子元素 ID 最多打印 10 个,超出部分以...结尾(见 print 实现)。

JSON 模式(--output=json,printJson()):输出一个 JSON 数组,每项字段为:

字段含义
id集合节点 ID
size集合 retained size(可读字节)
childrenSize集合直接子元素总数
staleChildrenSize其中 stale 的元素数量
staleChildrenIds全部 stale 元素节点 ID 列表

JSON 模式适合接入脚本或 Agent 做二次处理,例如批量提取staleChildrenIds后逐一追踪引用链。

从分析结果到定位泄漏源

collections-with-stale给出的节点 ID 可以直接衔接 memlab 的其它能力,形成一条完整的排查链路:

  1. 用memlab trace --node-id=<HEAP_OBJECT_ID>追踪某个 stale 元素或集合本身的 retainer 链,看它被哪条路径钉在堆上;
  2. 结合memlab analyze detached-DOM、unmounted-fiber-node等单快照分析交叉验证;
  3. 如果怀疑集合本身在持续膨胀(条目数单调增长),再切换到多快照的memlab analyze unbound-collection用--snapshot-dir观察一段时间序列。

小结

CollectionsHoldingStaleAnalysis是 memlab 中一个目标明确的单快照堆分析插件:它仅针对Map、Set、Array三类集合,通过table边穿透 Map/Set 的内部结构,用isDetachedDOMNode/isDetachedFiberNode判定直接子元素是否“陈旧”,最终按陈旧元素 retained size 输出 Top 20 命中集合。无论是通过 CLI(memlab analyze collections-with-stale --snapshot=... [--output=json])还是编程 API(analyzeSnapshotFromFile(file, {workDir}))调用,都能为“集合滞留 detached 对象”这一高频泄漏模式提供直接的量化线索。核心实现可进一步参阅 CollectionsHoldingStaleAnalysis.ts、BaseAnalysis.ts 与 core 的 utils。

  • 开发工具
  • 测试
  • CLI
  • MCP 服务

【免费下载链接】memlab

A framework for finding JavaScript memory leaks and analyzing heap snapshots

项目地址:https://gitcode.com/gh_mirrors/me/memlab
点击查看免费下载
上一篇:Alpine Linux 运维速查手册:apk 包管理、OpenRC 服务与容器化实践
下一篇:Manim 动画中的 Updaters 与 ValueTracker:构建逐帧响应的连续动画(manim-video 实战指南)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询