内存泄漏查找策略和技巧

本文档较旧,部分信息已过时。请谨慎使用。

查找泄漏的策略

在尝试使某个特定的测试用例不发生泄漏时,我建议首先关注最大的对象图(因为它们会包含许多较小的对象),然后关注较小的引用计数对象图,最后关注任何剩余的单个对象或不包含其他对象的较小对象图。

因为 (1) 泄漏的大型对象图往往包含一些由全局变量指向的对象,这些对象会迷惑基于 GC 的泄漏检测器,从而使泄漏看起来更小(如 bug 99180{.external .text} 中所示)或完全隐藏它们,以及 (2) 泄漏的大型对象图往往会隐藏较小的对象图,因此最好先处理大型泄漏对象图。

查找和修复泄漏的一个很好的通用模式是从您希望不泄漏的任务开始(例如,阅读电子邮件)。通过在 nsTraceRefcnt 日志记录下运行任务的一部分来开始查找和修复泄漏,逐步从尽可能少的部分构建到完整任务,并在添加额外步骤之前修复大部分泄漏。(在大部分泄漏中,我的意思是大量不同类型对象泄漏或已知会包含许多未记录对象的泄漏,例如 JS 对象。看到泄漏的 GlobalWindowImplnsXULPDGlobalObjectnsXBLDocGlobalObjectnsXPCWrappedJS 是一个迹象,表明可能存在大量泄漏的 JS 对象。)

例如,从打开邮件窗口并在不执行任何操作的情况下关闭窗口开始。然后继续选择文件夹,然后选择邮件,然后执行阅读邮件时执行的其他操作。

完成此操作后,如果泄漏不多,则尝试在 trace-malloc 或 LSAN 或 Valgrind 下执行该操作以查找较小对象图的泄漏。(当我提到对象图的大小时,我指的是对象的数量,而不是以字节为单位的大小。泄漏许多字符串副本可能是一个非常大的泄漏,但对象图很小,并且可以使用基于 GC 的泄漏检测轻松识别。)

我们有哪些泄漏工具?

工具 查找 平台 需要
大型对象图的泄漏工具
GC 和 CC 日志 JS 对象、DOM 对象、许多其他类型的对象 所有平台 任何构建
中等大小对象图的泄漏工具
BloatView引用计数跟踪和平衡 实现 nsISupports 或使用 MOZ_COUNT_{CTOR,DTOR} 的对象 所有一级平台 调试版本(或使用 --enable-logrefcnt 构建的优化版本)
用于调试在关闭时清除的内存增长的泄漏工具

常见的泄漏模式

在尝试查找引用计数对象的泄漏时,有一些模式可能会导致泄漏

  1. 所有权循环。难以修复的泄漏最常见的来源是所有权循环。如果可以避免一开始就创建循环,请务必这样做,因为在每一种情况下都确保打破循环通常很困难。有时这些循环会扩展到 JS 对象(下面将进一步讨论),并且由于 JS 是垃圾回收的,因此每个指针都像所有权指针一样,并且扩展的潜力更大。请参阅 bug 106860{.external .text} 和 bug 84136{.external .text} 以了解示例。(现在我们有了循环收集器,这个建议是否仍然准确?——Jesse)

  2. 通过以下方式丢弃引用:

    1. 忘记释放(因为您在应该使用 nsCOMPtr 时没有使用):请参阅 bug 99180{.external .text} 或 bug 93087{.external .text} 以了解示例,或 bug 28555{.external .text} 以了解更有趣的示例。在不使用 nsCOMPtr 的情况下,这在提前返回时也是一个常见问题。

    2. 双重 AddRef:这最常发生在将返回 AddRefed 指针(错误!)的函数的结果分配给 nsCOMPtr 而没有使用 dont_AddRef() 时。请参阅 bug 76091{.external .text} 或 bug 49648{.external .text} 以了解示例。

    3. [不常见] 对同一变量进行双重赋值:如果释放成员变量,然后通过调用另一个执行相同操作的函数对其进行赋值,则可能会泄漏内部函数分配给变量的对象。(无论是否使用 nsCOMPtr,都可能发生这种情况。)请参阅 bug 38586{.external .text} 和 bug 287847{.external .text} 以了解示例。

  3. 丢弃未引用计数的对象(尤其是拥有对引用计数对象引用的对象)。请参阅 bug 109671{.external .text} 以了解示例。

  4. 应该为虚函数的析构函数:如果希望覆盖对象的析构函数(包括为其派生类提供 nsCOMPtr 成员变量)并通过指向基类的指针使用 delete 删除该对象,则其析构函数最好是虚函数。(但我们在代码库中有很多不需要是虚函数的析构函数——不要这样做。)

调试通过 XPConnect 发生的泄漏

许多泄漏的大型对象图都通过 XPConnect{.external .text} 进行。这可能意味着将显示 XPConnect 包装器对象作为拥有泄漏的对象,但这并不意味着这是 XPConnect 的错误(尽管 已知会发生这种情况{.external .text},但很少见)。调试通过 XPConnect 发生的泄漏需要基本了解 XPConnect 的作用。XPConnect 允许将 XPCOM 对象公开给 JavaScript,并且允许将某些 JavaScript 对象作为普通的 XPCOM 对象公开给 C++ 代码。

当 C++ 对象公开给 JavaScript(两种情况中更常见的一种)时,会创建一个 XPCWrappedNative 对象。此包装器拥有对本机对象的引用,直到相应的 JavaScript 对象被垃圾回收。这意味着,如果存在可从其访问包装器的泄漏 GC 根,则包装器将永远不会释放其对本机对象的引用。虽然可以详细调试此问题,但解决这些问题的最快方法通常是简单地调试泄漏的 JS 根。这些根在 DEBUG 版本中关闭时打印,根的名称应给出与其关联的对象类型。

泄漏 JS 根的最常见方法之一是泄漏 nsXPCWrappedJS 对象。这是反方向的包装器对象——当 JS 对象用于实现 XPCOM 接口并被本机代码透明地使用时。 nsXPCWrappedJS 对象创建一个 GC 根,只要包装器存在,该根就存在。包装器本身只是一个普通的引用计数对象,因此可以使用正常的引用计数平衡工具调试泄漏的 nsXPCWrappedJS

如果您确实需要密切调试涉及 JS 对象的泄漏,则可以通过使用 bug 378261{.external .text} 和 bug 378255{.external .text} 中添加的函数来获取 JS 在确定活动对象集时用于标记对象的路径的详细打印输出。(GC_MARK_DEBUG 的这个替代方案(旧方法)的更多文档将很有用。它可能只是涉及将 XPC_SHUTDOWN_HEAP_DUMP 环境变量设置为文件名,但我还没有测试过。)

堆栈跟踪的后处理

在 Mac 和 Linux 上,我们的内部调试工具生成的堆栈跟踪没有非常好的符号信息(因为它们只显示 dladdr 的结果)。可以通过后处理来显著改进堆栈(更好的符号以及文件名/行号信息)。可以将堆栈通过脚本 tools/rb/fix_stacks.py 进行管道传输以执行此操作。这些脚本旨在用于平衡树以及原始堆栈;由于它们相当慢,因此通常更快地生成平衡树(例如,对于引用计数平衡器使用 make-tree.pl 或对于 trace-malloc 使用 diffbloatdump.pl --use-address)并然后运行平衡树(它们更小)进行后处理。

获取系统库的符号信息

Windows

将环境变量 _NT_SYMBOL_PATH 设置为类似 symsrv*symsrv.dll*f:\localsymbols*http://msdl.microsoft.com/download/symbols 的内容,如 Microsoft 的文章{.external .text} 中所述。这需要在运行时完成,因为我们在运行时执行地址到符号的映射。

Linux

许多 Linux 发行版提供了包含系统库外部调试符号的软件包。 fix_stacks.py 使用此调试信息(尽管它不会验证它们是否与系统上的库版本匹配)。

例如,在 Fedora 上,这些位于 *-debuginfo RPM 中(这些 RPM 可在默认情况下禁用的 yum 存储库中获得,但可以通过编辑系统配置轻松启用)。

技巧

禁用 Arena 分配

对于许多较低级别的泄漏工具(特别是基于 trace-malloc 的工具,如 leaksoup),如果可能,禁用您感兴趣的对象的 Arena 分配可能会有所帮助,以便每个对象都通过对 malloc 的单独调用进行分配。您可以执行此操作的一些位置是

布局引擎:在 layout/base/nsPresShell.cpp 中注释掉 DEBUG_TRACEMALLOC_FRAMEARENA 的位置定义它

glib:设置环境变量 G_SLICE=always-malloc

其他参考