查找由智能指针引起的内存泄漏

Posted

技术标签:

【中文标题】查找由智能指针引起的内存泄漏【英文标题】:Find memory leaks caused by smart pointers 【发布时间】:2010-09-09 04:36:35 【问题描述】:

有人知道发现智能指针引起的内存泄漏的“技术”吗?我目前正在开发一个用 C++ 编写的大型项目,该项目大量使用智能指针和引用计数。显然,我们有一些由智能指针引起的内存泄漏,它们仍然在代码中的某处被引用,因此它们的内存不会被释放。很难找到带有“不必要”引用的代码行,这会导致相应的对象不被释放(尽管它不再使用)。

我在网上找到了一些建议,建议收集引用计数器的递增/递减操作的调用堆栈。这给了我一个很好的提示,哪段代码导致引用计数器增加或减少。

但我需要某种算法,将相应的“增加/减少调用堆栈”组合在一起。删除这对调用堆栈后,我希望(至少)剩下一个“增加调用堆栈”,它向我显示了带有“不必要”引用的代码段,导致相应的对象未被释放。现在修复泄漏问题不大!

但是有人对进行分组的“算法”有想法吗?

Windows XP下进行开发。

(我希望有人理解,我试图解释的......)

编辑:我说的是循环引用引起的泄漏。

【问题讨论】:

不确定我是否理解...您能更具体地引用一种编程语言或平台吗?内存泄漏的处理方式可能会有所不同。 智能指针一般指C++/STL。 这里有些混乱。我认为值得区分没有指针的分配内存(智能指针不应该发生)、循环引用导致的泄漏,以及由于指针被保留而不必要地延长生命周期的对象。跨度> 【参考方案1】:

既然您说您使用的是 Windows,您或许可以利用 Microsoft 的用户模式转储堆实用程序 UMDH,它与 Debugging Tools for Windows 一起提供。 UMDH 制作应用程序内存使用情况的快照,记录每次分配使用的堆栈,并让您比较多个快照以查看对分配器的哪些调用“泄漏”了内存。它还使用 dbghelp.dll 将堆栈跟踪转换为符号。

还有另一个名为“LeakDiag”的 Microsoft 工具,它支持比 UMDH 更多的内存分配器,但它有点难找,而且似乎没有得到积极维护。如果我没记错的话,最新版本至少有五年的历史了。

【讨论】:

【参考方案2】:

第一步可能是知道哪个类正在泄漏。 一旦你知道了,你就可以找到谁在增加引用: 1.在shared_ptr包裹的类的构造函数上下断点。 2. 在 shared_ptr 增加引用计数时使用调试器介入:查看变量 pn->pi_->use_count_ 通过评估表达式获取该变量的地址(类似这样:&this->pn->pi_.use_count_),您将获得一个地址 3.在visual studio调试器中,进入Debug->New Breakpoint->New Data Breakpoint... 输入变量的地址 4. 运行程序。每当代码中的某个点增加和减少引用计数器时,您的程序都会停止。 然后你需要检查它们是否匹配。

【讨论】:

【参考方案3】:

我所做的是将智能指针包装在一个带有 FUNCTIONLINE 参数的类中。每次调用构造函数时增加该函数和行的计数,每次调用析构函数时减少计数。然后,编写一个转储函数/行/计数信息的函数。这会告诉您所有引用的创建位置

【讨论】:

我使用过这种技术。如果一切最终都来自 CreateObj,则传递 CreateObj 的 FUNCTION/LINE 的调用者。 (当然,通过宏来做到这一点。如果你有一个函数“谁叫我?”就更好了) @Krazy/Joe:你能指出一些例子来说明如何找到“谁给我打电话?” 识别调用者只是堆栈跟踪的第一部分,因此***.com/questions/77005/… 可以满足您的需求。 Backtrace() / execinfo.h. 在我当前的项目中,我懒得做execinfo(实际上我的同事并不想添加stacktrace代码),所以我只是分叉了gdb,即Gnu Debugger,并使用gdb来提供来电者信息。 (警告:我了解到 pthread 中存在导致分叉挂起的错误,这在 POSIX 中本不应该发生。)至少在我们使用的旧版本的 Linux 中是这样。 // 虽然我不会将 GDB 用于任何我想要快速的事情。【参考方案4】:

要检测引用循环,您需要拥有所有引用计数对象的图表。这样的图并不容易构建,但可以做到。

创建一个全局set<CRefCounted*> 来注册活引用计数对象。如果您有通用的 AddRef() 实现,这会更容易 - 当对象的引用计数从 0 变为 1 时,只需将 this 指针添加到集合中。类似地,在 Release() 中,当引用计数从 1 变为集合时,从集合中删除对象0.

接下来,提供一些方法来从每个CRefCounted* 中获取引用对象集。它可以是virtual set<CRefCounted*> CRefCounted::get_children() 或任何适合您的。现在你有一种方法来遍历图表了。

最后,为cycle detection in a directed graph 实现你最喜欢的算法。启动程序,创建一些循环并运行循环检测器。享受! :)

【讨论】:

我喜欢你的思维方式,但我认为没有一种简单的方法可以从对象中获取引用的对象:get_children @lzprgmr,是的,get_children() 必须根据班级维护的引用知识为每个参与班级手动编码。这就是我所说的“不容易构建”。【参考方案5】:

请注意,具有引用计数智能指针的泄漏源之一是具有循环依赖项的指针。例如,A 有一个指向 B 的智能指针,B 有一个指向 A 的智能指针。A 和 B 都不会被销毁。你必须找到,然后打破依赖关系。

如果可能,请使用 boost 智能指针,并将 shared_ptr 用于应该是数据所有者的指针,而使用 weak_ptr 用于不应该调用 delete 的指针。

【讨论】:

【参考方案6】:

如果您可以确定性地重现泄漏,我经常使用的一种简单技术是按照构造顺序对所有智能指针进行编号(在构造函数中使用静态计数器),并将此 ID 与泄漏一起报告.然后再次运行程序,在构造相同 ID 的智能指针时触发 DebugBreak()。

你也应该考虑这个很棒的工具:http://www.codeproject.com/KB/applications/visualleakdetector.aspx

【讨论】:

您可以与堆栈转储一起执行此操作。这将允许您通过 refs/derefs 的 UID 配对输出【参考方案7】:

我是Google's Heapchecker 的忠实粉丝——它不会捕获所有泄漏,但它会捕获大部分泄漏。 (提示:将其链接到您的所有单元测试中。)

【讨论】:

【参考方案8】:

这不是发现泄漏的问题。在智能指针的情况下,它很可能会指向一些通用的地方,比如 CreateObject(),它被调用了数千次。这是一个确定代码中的哪个位置没有在引用计数的对象上调用 Release() 的问题。

【讨论】:

【参考方案9】:

对于 Windows,请查看:

MFC Memory Leak Detection

【讨论】:

【参考方案10】:

我为解决这个问题所做的是覆盖 ma​​lloc/newfree/delete 运算符,以便它们尽可能地在数据结构中跟踪您正在执行的操作。

例如,当覆盖 ma​​lloc/new 时,您可以创建调用者地址、请求的字节数、返回的分配指针值和序列 ID 的记录,这样您的所有记录都可以已排序(我不知道您是否处理线程,但您也需要考虑到这一点)。

在编写free/delete 例程时,我还会跟踪调用者的地址和指针信息。然后我向后查看列表并尝试使用指针作为键来匹配 ma​​lloc/new 对应项。如果我没有找到它,请举起红旗。

如果您负担得起,您可以在数据中嵌入序列 ID,以绝对确定谁以及何时进行了分配调用。这里的关键是尽可能地唯一标识每个交易对。

然后您将有第三个例程显示您的内存分配/释放历史,以及调用每个事务的函数。 (这可以通过从链接器中解析符号映射来完成)。您将随时知道您将分配多少内存以及是谁分配的。

如果您没有足够的资源来执行这些事务(我的典型案例是 8 位微控制器),您可以通过串行或 TCP 链接将相同的信息输出到具有足够资源的另一台机器。

【讨论】:

【参考方案11】:

如果我是你,我会记录日志并编写一个快速脚本来执行以下操作(我的是 Ruby):

def allocation?(line)
  # determine if this line is a log line indicating allocation/deallocation
end

def unique_stack(line)
  # return a string that is equal for pairs of allocation/deallocation
end

allocations = []
file = File.new "the-log.log"
file.each_line  |line|
  # custom function to determine if line is an alloc/dealloc
  if allocation? line
    # custom function to get unique stack trace where the return value
    # is the same for a alloc and dealloc
    allocations[allocations.length] = unique_stack line
  end


allocations.sort!

# go through and remove pairs of allocations that equal,
# ideally 1 will be remaining....
index = 0

while index < allocations.size - 1
  if allocations[index] == allocations[index + 1]
    allocations.delete_at index
  else
    index = index + 1
  end
end

allocations.each  |line|
  puts line

这基本上会遍历日志并捕获每个分配/解除分配并为每对存储一个唯一值,然后对其进行排序并删除匹配的对,看看还剩下什么。

更新:对所有中间编辑感到抱歉(我在完成之前不小心发布了)

【讨论】:

注意,我假设您已经有一个日志,其中包含您在问题中谈到的 alloc/free 跟踪,但其他答案可能更好地帮助您解决一般的内存泄漏【参考方案12】:

我这样做的方式很简单: - 在每个 AddRef() 记录调用堆栈上, - 匹配 Release() 将其删除。 这样,在程序结束时,我只剩下 AddRefs() 而没有处理 Releases。无需配对,

【讨论】:

以上是关于查找由智能指针引起的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

智能指针的原理和实现

智能指针(解决你的内存泄漏)

智能指针(解决你的内存泄漏)

智能指针(解决你的内存泄漏)

C++智能指针详解:智能指针的引入

C++智能指针