帮助奇怪的记忆行为。寻找我的大脑和我的代码中的泄漏

时间:2010-12-21 12:09:44

标签: c++ debugging memory memory-leaks

我花了最近几天试图在我们正在开发的程序中找到内存泄漏。

首先,我尝试使用一些检漏仪。在解决了一些问题后,他们不再发现任何泄漏。但是,我也在使用perfmon.exe监控我的应用程序。当使用该应用程序时,性能监视器报告“私有字节”和“工作集 - 私有”正在稳步上升。对我而言,这表明程序运行的时间越长,内存越来越多。然而,内部资源似乎很稳定,所以这听起来像是泄露给我。

程序正在运行时加载DLL。我怀疑这些泄漏或它们在该库中发生的任何泄漏,并在卸载库时被清除,因此它们不会被泄漏检测器拾取。我使用DevPartner BoundsChecker和Virtual Leak Detector来查找内存泄漏。两个都应该捕获DLL中的泄漏。

此外,内存消耗逐步增加,这些步骤大致但不完全一致,与我在应用程序中执行的某些GUI操作一致。如果这些是我们代码中的错误,那么每次执行操作时都会触发它们,而不是大部分时间

每当我面对如此多的陌生感时,我开始质疑我的基本假设。所以我转向你,谁知道一切,建议。我的假设有缺陷吗?您是否了解如何解决此类问题?

修改
我目前在Windows 7 64上使用Microsoft Visual C ++(x86)。

EDIT2:
我刚刚使用IBM Purify来寻找泄漏。首先,它将整个程序的30%列为泄漏内存。这不可能是真的。我想它是将整个DLL识别为泄露或类似的东西。但是,如果我每隔几次操作就会搜索新的泄漏,它会报告与性能监视器报告的大小增加相对应的泄漏。这可能导致泄漏。可悲的是,我只使用Purify的试用版,因此它不会向我显示这些泄漏的实际位置。 (这些泄漏仅在运行时出现。当程序退出时,任何工具都不会报告任何泄漏。)

5 个答案:

答案 0 :(得分:3)

如果没有看到你的代码就很难知道,但是在C ++程序中出现“泄漏”的方法并不那么明显,例如。

  1. 内存碎片 - 如果你一直在分配不同大小的对象,那么有时候就不会有足够大的连续可用内存区域,而且必须从操作系统中分配更多。在释放分配中的所有内存之前,这样的分配将不会被释放回操作系统,因此长时间运行的程序将随着时间的推移而增长(就使用的地址空间而言)。

  2. 忘记在具有虚函数的基础案例中使用虚拟机 - 这是一个非常常见的问题导致泄漏。

  3. 使用智能指针,例如shared_ptr,并且有一个对象保持对象的shared_ptr - 内存泄漏工具通常不会发现这种事情。

  4. 使用智能指针并获取循环引用 - 您需要使用例如一个弱的地方,以打破周期。

  5. 对于工具,有净化效果好但价格昂贵。

答案 1 :(得分:3)

使用PerfMon或任务管理器监控应用程序的内存使用情况不是检查内存泄漏的有效方法。例如,您的运行时可能只是为了预分配或由于碎片而从操作系统中保留额外的内存。

根据我的经验,技巧是CRT调试堆。您可以请求有关所有活动对象的信息,CRT提供比较快照的功能。

http://msdn.microsoft.com/en-us/library/wc28wkas.aspx

答案 2 :(得分:2)

Perfmon可以让你知道你是否漏水,但这是原始的。有些商业产品会做得更好。我使用AQTime for C ++代码,它非常出色:http://www.automatedqa.com/products/aqtime/

它会告诉你分配泄漏内存的代码行。

答案 3 :(得分:1)

Perfmon查看分配给您的程序的(4K)页数。这些通常由堆管理器管理。例如,如果按下按钮需要3个1 KB的分配,堆管理器将不得不在前三次请求新页面。第四次,还剩3KB。因此,您无法断定每次按下按钮必须具有外部可见效果。

答案 4 :(得分:1)

我有一种非传统技术可以帮助找到代码中的可疑泄漏,我已经无数次地使用它并且非常有效。显然,这不是发现泄漏的唯一或最佳方式,但这是你应该在你的包里拿到的技巧。

根据您对代码的深入了解,您可能会想到几个可疑点。我过去所做的是通过(我称之为)放大泄漏来瞄准那些可疑点。这是通过简单地在可疑点周围放置一个循环来完成的,因此它不是一次调用,而是多次调用,通常是数千次,但这取决于底层分配的大小。诀窍是知道在哪里放置循环。通常,您希望将调用堆栈向上移动到循环内的所有已分配内存应该被释放的位置。在运行时使用perfmon来监视私有字节和工作集,当它发出尖峰时你就发现了泄漏。从那时起,您可以将调用堆栈中的循环范围缩小到泄漏点。

考虑以下示例(尽管可能很蹩脚):

char* leak()
{
    char* buf = new char[2];
    buf[0] = 'a';
    buf[1] = '\0';
}

char* furtherGetResults()
{
    return leak();
}

std::string getResults(const std::string& request)
{
    return furtherGetResults();
}

bool processRequest(SOCKET client, const std::string& request)
{    
    std::string results;

    results = getResults(request);

    return send(client, results.c_str(), results.length(), 0) == results.length();
}

如果代码分布在不同的模块中,甚至分布在不同的dll中,那么找到泄漏并不容易。它也很难找到,因为泄漏很小,但随着时间的推移可能会变大。

首先,你可以把循环放在调用getResults():

bool processRequest(SOCKET client, const std::string& request)
{    
    std::string results;

    for (size_t i = 0; i < 1000000; i++) {
        results = getResults(request);
    }

    return send(client, results.c_str(), results.length(), 0) == results.length();
}

如果内存使用率达到高峰,那么你就会遇到泄漏问题,接下来你将调用堆栈向下移动到getResults(),然后再调整到getGetResults()等等,直到你将其命名为止。这个例子过度简化了技术,但在生产代码中,每个函数调用的代码通常都要多得多,而且缩小范围会更加困难。

此选项可能并不总是可用,但是当它可以很快找到问题时。