检测引用计数对象中的内存泄漏

Posted

技术标签:

【中文标题】检测引用计数对象中的内存泄漏【英文标题】:Detecting memory leak in reference counted objects 【发布时间】:2012-10-01 08:52:12 【问题描述】:

我正在尝试打印调用 addref 和 release 的哪一行。这里是代码

在下面的代码中,我创建了 ReferenceCount 类,其主要功能是增加和减少引用计数。 Referencemanager 类跟踪引用计数并在达到 0 时删除对象。

Test1 是测试类。在 main 中,我正在创建 Test1 指针并用 CReferenceManager 类包装它。现在在创建 CReferenceManager 类时调用 AddRef 并在销毁时调用 Release。

如果存在内存泄漏,那么当 AddRef 和 Release 调用时使用引用计数时,我是否可以打印出 FILE 和 LINE 编号会更容易检测。

如果有一种方法可以打印调用 AddRef 和 Release 的 FILE 和 LINE 编号。一种方法是我可以覆盖派生类中的 AddRef 和 Release 以及 prinf FILE 和 LINE 编号

//ReferenceCount.h
#include <string>
#include <Windows.h>

using namespace std;
class CReferenceCount

public:
   CReferenceCount();
   virtual ~CReferenceCount();
   virtual void AddRef();
   virtual bool Release();


private:
   LONG m_ref;

;


// RefCount.cpp 
//

#include "stdafx.h"
#include "ReferenceCount.h"


CReferenceCount::CReferenceCount():m_ref(0)

   AddRef();



CReferenceCount::~CReferenceCount()



void CReferenceCount::AddRef()

    InterlockedIncrement(&m_ref);


bool CReferenceCount::Release()

   if (InterlockedDecrement(&m_ref) == 0)
   
      delete this;
      return true;
   

   return false;




//ReferenceManager.h
#include <string>
#include <Windows.h>

using namespace std;
class CReferenceCount

public:
   CReferenceCount();
   virtual ~CReferenceCount();
   virtual void AddRef();
   virtual bool Release();


private:
   LONG m_ref;

;

//test.cpp
#include "stdafx.h"
#include "ReferenceCount.h"
#include "RefManager.h"
#include <iostream>
using namespace std;

class Test1: public CReferenceCount

public:
    Test1()
    ~Test1()

private :
    int m_i;
;

void main()

    Test1 *pTest= new Test1();
    CReferenceManager<Test1> testRef(pTest);


我发布的类似问题 finding who creates object via smart pointer Design pattern to detect memory leaks for reference counted smart pointers

但没有一个答案给出正确的解释来解决这个问题,

【问题讨论】:

delete this; OMG!!! 您是使用智能指针调用 AddRef / Release 还是手动调用它们?如果您手动调用它们,我强烈建议您不要这样做。 模板会是更好的解决方案;可以使模板适合每个对象,就像 std::shared_ptr @std''OrgnlDave : 你能给我举个例子吗 @BЈовић parashift.com/c++-faq-lite/delete-this.html :) 【参考方案1】:

简短的回答:您应该使用其他人发布的想法,即使用 ADD/RELEASE 宏并将编译器提供给您的跟踪类的预定义 __FILE__ 和 __LINE__ 宏。

略长一点的答案:您还可以使用允许您遍历堆栈并查看谁调用了该函数的功能,这比使用宏更灵活、更简洁,但几乎可以肯定更慢。

此页面向您展示如何在使用 GCC 时实现此目的:http://tombarta.wordpress.com/2008/08/01/c-stack-traces-with-gcc/。

在 Windows 中,您可以使用一些编译器内在函数以及符号查找功能。详情请查看:http://www.codeproject.com/tools/minidump.asp

请注意,在这两种情况下,您的程序都需要至少包含一些符号才能使其正常工作。

除非您对在运行时执行此操作有特殊要求,否则我建议您查看简短答案。

【讨论】:

【参考方案2】:

唯一的方法是定义宏来调用 AddRef 和 Release,因为函数无法在内部知道它们是从哪里调用的。所以你可以使用类似的东西。

#define RELEASE(obj) cout << __LINE__ << ":" << __FILE__ << endl; (obj).Release();

另外,不同的编译器有不同的预定义宏;如果可移植性是一个问题,那么在编写上述代码时应该考虑这一点。 MSDN reference (2003)

鉴于您在下面的 cmets,我可能会提供另一种有点骇人听闻的解决方案。您可能无法看到您的参考文件是在哪里发布的,但您可以获得有关它的创建位置以及未正确发布的更多信息。

template <typename T>
struct CReferenceManager

    CReferenceManager(const T & _obj, const string & _file, int _line) : mObj(_obj), mFile(_file), mLine(_line)
    
        cout << "Constructing from " << _file << ":" << _line << endl;
        CReferenceManager::sObjects[make_pair(mFile, mLine)]++;
        mObj.addRef();
    

    ~CReferenceManager()
    
        cout << "Destructing object created at " << mFile << ":" << mLine << endl;
        CReferenceManager::sObjects[make_pair(mFile, mLine)]--;
        mObj.Release();
    

    static map<pair<string, int>, int> sObjects;
    string mFile;
    int mLine;
    T obj;


int main()

...
    // Cycle through sObjects before return, note any unreleased entries
    return 0;

请注意,这只是伪代码;我怀疑它是否可以编译或开箱即用!

【讨论】:

这是我昨天在这里建议的:***.com/a/12806087/241536 如果我使用这个宏,它会将 FILE 打印为 RefManager 而不是 test.cpp。我的意图是打印 test.cpp 及其从 Release 被调用的行号。 除了 FILELINE 你试过 FUNCTION_ 吗? FUNCTION 也很有用(我在我自己的宏版本中使用它)@Alien01,我认为你不能做你想做的事。如果 main() 调用 foo() 调用 bar() 再调用 release(),那么 release 怎么会知道你真正感兴趣的调用堆栈有多远?它是否打印 foo()、bar() 或 main() 的行?当您考虑到您基本上是在要求调用析构函数的行时,这变得更加麻烦;可能是“抛出”或“返回”,但无法从 ref 管理器的析构函数中得知。【参考方案3】:

按照您的要求进行操作的一种方法是通过 AddRef 并使用以下方式发布此信息:

void CReferenceCount::AddRef(const char *file=0, int line=-1)  if (file) cout << "FILE:" << file; if (line>0) count << " LINE: " << line;   .... do the rest here ... 

那么当你调用函数的时候,你可以使用类似于上面Rollie建议的宏,像这样:

#define ADDREF(x) x.AddRef(__FILE__, __LINE__)

这将传递进行调用的文件和行,我相信这是您所要求的。您可以控制要对方法中的信息执行的操作。正如我在上面所做的那样,将它们打印出来只是一个例子。您可能希望收集除此之外的更多信息,并将其记录到另一个对象,这样您就有了呼叫历史记录,将它们写入日志文件等。您还可以从呼叫点传递更多信息,而不仅仅是文件和线,根据您需要的跟踪类型和级别。默认参数还允许您在不传递任何内容的情况下使用它们(通过简单的宏重新定义),只是为了查看最终版本的行为方式,其中包含两次堆栈推送和两次条件检查的开销。

【讨论】:

就像在对 Rollie 答案的评论中指出的那样,这不起作用,因为 OP 需要将宏调用放在他的 CReferenceManager 模板类中。因此宏将始终打印 RefManager 作为文件名。 嗯,没有。如果宏在头文件中,它将起作用,并且在其他几个 cpp 文件中使用。 此处的空间不足以粘贴示例运行,但如果有人感兴趣,我可以添加答案。 DNT@ :当然我会对示例非常感兴趣。【参考方案4】:

对于我参与的项目,我有类似的需求。我们有自己的智能指针模板类,并且由于循环引用不时出现内存泄漏。

要知道引用泄漏对象的智能指针仍然存在(2 个或更多),我们使用特殊的预处理器定义编译源代码,该定义在智能指针实现中启用特殊的调试代码。你可以看看我们的smart-pointer class。

本质上,每个智能指针和引用计数对象都有一个唯一的 id。当我们获得泄漏对象的 id 时(通常使用 valgrind 来识别泄漏对象的内存分配的源位置),我们使用我们的特殊调试代码来获取引用该对象的所有智能指针 id。然后我们使用一个配置文件,在其中写下智能指针 ID,在下一次应用程序启动时,我们的调试工具会读取该文件,然后它会知道它应该为哪个新创建的智能指针实例触发一个信号以进入调试器。这揭示了创建该智能指针实例的堆栈跟踪。

诚然,这涉及一些工作,并且可能只为大型项目带来回报。

另一种可能性是在运行时在 AddRef 方法中记录堆栈跟踪。查看我的 ctkBackTrace 类以在运行时创建堆栈跟踪。用标准 STL 类型替换 Qt 特定类型应该很容易。

【讨论】:

【参考方案5】:

引用计数的原理是当用户链接到对象时增加计数器,当他们断开链接时减少。

所以你必须:

操纵智能指针,而不是使增加/减少透明的指针 重载 smart_pointer 的复制构造函数和赋值运算符

象征性的例子:

A a = new A(); refcount = 0,没人用 Link&lt;A&gt; lnk( a ); refcount = 1 obj.f( lnk );obj 存储 lnk, refcount = 2 此方法可能会返回,因为所有权已转移到obj

所以,看看参数传递(可能会自动复制)和复制到外部对象中。

CORBA 星云中有很好的教程。

您还可以看到ACE 或ICE 或0MQ。

【讨论】:

【参考方案6】:

您不应该在自己的代码中显式分配或释放引用,因此存储源文件和引用递增或递减的行根本不会帮助您,因为这些将(应该!)总是在引用计数管理代码。

您没有将源代码包含到您的 CReferenceManager 类中,但根据您的描述,它是引用计数对象的包装器。它是否正确?此 CReferenceManager 对象的正确实现应确保:

采用裸指针的构造函数存储指针并且不更改引用计数(因为您的 CReferenceCount 类创建具有一个引用的对象) 引用总是在析构函数中递减 引用在复制构造函数中递增 在赋值运算符中,右侧对象的引用递增,左侧对象的引用递减 不应公开显式递增/递减参考方法 operator->() 方法应该返回指向对象的指针 应该没有直接的方法可以将引用计数对象从拥有它的 CReferenceManager 实例中分离出来。唯一的方法是分配一个新的引用计数对象。

此外,您希望将 CReferenceCount 类中的 AddRef()Release() 方法设为私有,并通过类友谊使它们只能由 CReferenceManager 类访问。

如果您在 CReferenceManager 类中遵循上述规则,则可以通过确保每个人都通过分配在堆栈上的 CReferenceManager 包装器访问对象来避免泄漏或其他内存问题。换句话说:

要创建一个新的引用计数对象,请将一个新创建的对象(带有一个引用)传递给堆栈分配的 CReferenceManager 对象。示例:

CReferenceManager<Test1> testRef(new Test1());

要将对象作为参数传递给另一个函数或方法,请始终按值传递 CReferenceManager 对象(不是通过引用,也不是通过指针)。如果您这样做,复制构造函数和析构函数将负责为您维护引用计数。示例:

void someFunction(CReferenceManager<Test1> testObj)

    // use testObj as if it was a naked pointer
    // reference mananagement is automatically handled
    printf("some value: %d\n", testObj->someValue());


int main()

    CReferenceManager<Test1> testRef(new Test1());
    someFunction(testRef);

如果您需要将引用计数对象粘贴到容器中,则按值插入 CReferenceManager 包装器(不是其指针,也不是对象的裸指针)。示例:

std::vector< CReferenceManager<Test1> > myVector;
CReferenceManager<Test1> testRef(new Test1());
myVector.push_back(testRef);
myVector[0]->some_method(); // invoke the object as if it was a pointer!

我相信如果您严格遵守上述规则,您会发现的唯一问题是引用计数实现中的错误。

遵循这些规则的示例实现在 this page 中,尽管该解决方案缺乏对多线程保护的任何支持。

我希望这会有所帮助!

【讨论】:

【参考方案7】:

我想通过一些工作并使用 libunwind 您可能会尝试获得所需的东西(非常感谢)。

http://www.nongnu.org/libunwind/docs.html

【讨论】:

【参考方案8】:

有一些方法可以做到这一点,但首先让我问你一件事。为什么要手动管理引用并为内存泄漏提供机会?您可以轻松地使用boost::intrusive_ptr 为您完成这项工作?(如果您不想要提升,没有问题,请参阅intrusive_ptr 的实现并实现您自己的类或将其复制到您自己的文件中)和那你就没有内存泄漏去搜索了!!

但是作为您问题的答案,您可以有 2 个AddRef/Release 一个用于调试版本,另一个用于发布,您应该将AddRef 位置添加到std::stack 之类的结构中,然后在Release 上从@987654327 中弹出它们@ 最后你会看到堆栈中还有多少来自女巫位置的引用!但如果这是用于 COM 实现,请记住 COM 可能会多次调用 AddRef,然后稍后将其删除,因此您无法理解哪个 AddRef 没有对应的 Release

【讨论】:

以上是关于检测引用计数对象中的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

引用计数和内存泄漏的错误减少

JavaScript的垃圾回收机制与内存泄漏

swift 学习之自动引用计数

python如何进行内存管理

判断一个对象是否可以被回收

垃圾回收 及 内存泄漏