2013-09-03 内存泄漏问题解决总结

最近花了很多时间解决一个Memory Leak的问题。虽然原本的问题是一个智能指针循环引用导致的leak的问题,不过在调查过程中,对memory leak有了更多的理解和经验。记录下来,希望自己以后有所参考,也希望对别人遇到类似问题有所帮助。

内存泄漏问题典型的表现,就是随着程序只需运行,进程所占内存越来越多。导致的后果可能有2个,1是进程崩掉,2是进程hang住。
针对不同的场景,有的比较容易重新,有的不容易重新,关键是多久达到了内存峰值。

对于内存泄漏问题,先简化问题,最好使用脚本来创新。因为大部分情况下都是组件代码写的不好,导致的leak。用脚本重新,可以排除很多其他的干扰因素。比如用vbs重现,就会给分析leak point有很大的帮助。

在脚本中,对特定的调用重复多次,用Performance Monitor看进程所占的内存增长曲线。如果对于不确定应该monitor那个对象,那就把
Private Bytes, Memory Set, Virtual Bytes全加上。一般这几个线路都是类似升降的。
如果可以在脚本里面解决,那么说明就是调用组件的程序实现的问题。如果很不幸,是组件的问题,那么就可以使用下面的方法来定位是哪里出了问题。

memory leak的根源无非是malloc的内存没有释放,或者高级一点的创建的对象没有释放(其实还是分配的内存没有释放)。先来看看malloc的内存泄漏是怎么发现和解决的。

1. malloc 内存泄漏的发现和解决

大部分内容参考这个 link

首先,在要调查组件的某个重点怀疑对象的头文件里面(或者在stdafx.h)添加如下代码,来打开debug memory allocate的功能。

    #ifdef _DEBUG
        #define _CRTDBG_MAP_ALLOC
        #include <stdlib.h>
        #include <crtdbg.h>
    #endif

然后,在调用过程结束的时候,例如最上层调用结束的时候(考虑上最上层对象的析构),添加下面的代码,来进行leaked memory的dump工作。
例如:

void CTopLevelClass::FinalRelease()    
{
    ... // other codes
    #ifdef _DEBUG
        _CrtDumpMemoryLeaks();
    #endif
}

然后,用Visual Studio attach到运行的进程上。当执行到_CrtDumpMemoryLeaks()的时候,在Output窗口会打印出很多类似下面的信息

Detected memory leaks!
Dumping objects ->
<path of the source file>(157) : {5846} normal block at 0x00000000042FDEF0, 136 bytes long.
 Data: <                > 00 00 00 00 00 00 00 00 CD CD CD CD CD CD CD CD 
{5845} normal block at 0x00000000042F9F70, 40 bytes long.
 Data: <          /     > A0 D8 BD 04 00 00 00 00 F0 DE 2F 04 00 00 00 00 
......
Object dump complete.

将这些内容保存到一个文本文件,会更方面查看。如果足够幸运,我们可以直接从提示的代码行看到泄漏的地方。
如果不容易看出,那么我们需要继续更深入的探索,那些对象是怎么泄漏的。如上面的例子,我们看到泄漏的memory block编号是 {5846}
所以我们需要在程序运行比较靠前的某个时刻(至少是这个对象申请之前的某个时刻,例如Top level对象的构造函数中),添加下面的代码

CMyClass()    
{ 
     ....
    _CrtSetBreakAlloc(5846);
}

这段代码会在这个memory block allocate的时候添加断点。断点就会在真实分配的时刻起作用,然后我们就查看callstack,来判断这个对象为什么没有被释放。

这个技巧可能更适合于以c++标准库为开发基础的project。但如果是以ATL COM为基础,那么,事情就会复杂一些。下面的章节再进行讨论。

2. ATL COM中的内存泄漏

ATL COM是类似于MFC的轻量级c++库,因为引入了智能指针,所以对于指针不需要像标准库使用中那么担惊受怕。但,还是会有leak。所以了解智能指针的原理,还有是BSTR相关类的使用,才能更好的避免memory leak的发生。

2.1 工具

常用的工具有IBM Purify Plus, AQTime, LeakDiag, BounderChecker, LeakDiag, 以及下面重点推荐的UMDH。
如果是简单明显的leak,这些工具还是很给力,直接就给出了leak的代码行。具体使用其实很简单,就不多说了。

目前用的比较多的,是UMDH。UMDH是Debugging Tools for Windows工具开发部里面的一个。
Microsoft出品,参考 link

辅助使用的还有
gflag.exe - 主要用来dump某一个时刻的整个内存内容(堆上的)
umdh.exe - 主要用来比较两个内存dump的差异
因此,常用的方法就是写一个脚本(或者运行有leak的程序),再重复操作的某个过程的开始加上一个停顿(可以是messagebox,也可以是breakpoint),dump一次,下一次停顿的时候,再dump一次,然后使用umdh进行内存比较,看这段过程内存中什么东西增加了,由此来判断什么内存对象leak了。
例如.bat文件就可以写成这样

//main bat file
echo off
gflags /i wscript.exe +ust
gflags /i wscript.exe /tracedb 24
set _NT_SYMBOL_PATH=SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols
echo setting success
cd c:\test\
wscript a.vbs
echo differing.......
umdh -d -v -l a.txt b.txt>diff.txt
del a.txt
del b.txt
pause

然后第一次断点触发的时候运行的bat

tlist -p wscript.exe >pid
set /P CUR_PID=<pid
umdh -p:%CUR_PID% -f:a.txt
del pid

第二次断点触发的时候运行的bat

tlist -p wscript.exe >pid
set /P CUR_PID=<pid
umdh -p:%CUR_PID% -f:b.txt
del pid

用于profiling的vbs类似于这样

MsgBox "start"
Dim idx
For idx = 1 to 5 
    'run some code here
next
MsgBox "end"

大部分情况下,umdh的输出会有一些重复段落。默认它会根据leak的总大小来排序,所以一般先看第一个,逐个fix。
当可以定位到代码行的时候,就需要理由真才实学来进行代码的fix了。下面是一些如何判断是否为leak的方法,已经常见的leak以及fix solution。

2.2 memory leak的判断

一般来说,leak的对象无非是对象,或者string,而且一般都是由于指针的不正确使用导致的。
首先需要明确一个概念,BSTR s = L"hello"; 这个字符串是创建再调用堆栈上的,不需要担心它是否leak,因为堆栈运行退出的时候自然就会清理掉。
但 BSTR s1 = SysAllocString(L"world");是需要担心的,因为这个string是创建在堆中的,堆栈调用只引用它的指针,因此如果堆栈运行跳出后如果没有清理,那么这个string对象就成了没人要的孩子,就leak了。
还有一种情况,是调用了.Copy()或者CopyTo(),在Copy的时候,背后也会调用SysAllocString(),因此接受的对象也需要负起责任来。

调用CComBSTR bstr2 = bstr1.Copy(),或者CComBSTR bstr3(bstr1.Copy())的时候也要特别小心。 因为这一个句子里面有两个StrAllockString(),我们不担心bstr1的leak,也不担心bstr2的leak,因为他们都有人负责,但bstr1的Copy的那个string,是没有人负责的,必leak无疑。

总的来说,代码里面有看到StrAllocString的地方,Copy/CopyTo的地方,都需要格外小心。

还有一种情况,就是对象指针的循环引用。尤其是parent保持了Child的智能引用指针,而child也保存了parent的智能引用指针,这将导致parent和child都leak。这种情况是比较难从umdh的结果里面看出来的。在这种情况下要遵循的原则就是parent堆child的引用要用智能指针,child对parent的引用要用裸指针。

有时候,umdh会有一些误报。这些误报是因为BSTR的cache引起的。这个不算是leak,真实场景也不会引起问题,只是umdh的谎报而已。为了更精确umdh的输出,最好设置环境变量OANOCACHE=1,可以参考 linklink.

还有一个情况,在函数调用的时候,参数会被copy,尽量保证在函数内部对传入的非空参数不要进行修改。否则很容易泄漏(参考后面的例子)

2.3. 常见memory leak的code以及fix solution

Case 1:

CComBSTR s;
InnerMakeName(L"", &s);
InnerMakeName(L"", &s);
InnerMakeName(L"", &s);

Fix solution to case 1:

CComBSTR s;
InnerMakeName(L"", &s);
s.Empty();
InnerMakeName(L"", &s);
s.Empty();
InnerMakeName(L"", &s);

Reason: 前两次的string对象没有指针指向了,所以就leak了。所以在函数中对输出参数进行非空和无值判断很重要,可以避免很多问题。

Case 2:

CComBSTR str1(L"hello, world");
BSTR str2;
str1.CopyTo(&str2);

Fix solution to Case 2

CComBSTR str1(L"hello, world");
BSTR str2;
str1.CopyTo(&str2);
CComBSTR f;
f.Attach(str2);

Fix solution 2 to case 2

CComBSTR str1(L"hello, world");
CComBSTR str2;
str1.CopyTo(&str2);

Case 3:

CComBSTR str1(L"hello, world");
CComBSTR str2 = str1.Copy();

fix solution to case 3:

CComBSTR str1(L"hello, world");
BSTR str2 = str1.Copy();
SysFreeString(str2);

fix solution to case 3

CComBSTR str1(L"hello, world");
CComBSTR str2 = str1.Copy();
SysFreeString(str2);

Case 4:

CComBSTR sIn = SysAllocString(L"hello world");
InnerMakeName(sIn, &sOut);
SysFreeString(sIn); //not work

fix solution to case 4:

BSTR sIn = SysAllocString(L"hello world");
InnerMakeName(sIn, &sOut);
SysFreeString(sIn);

Case 5:

CString s(L"hello");
InnerMakeName(s.AllocSysString(), &sOut);

fix solution to case 5:

CString s(L"hello");
CComBSTR s1 = s
InnerMakeName(s1, &sOut);

Case 6:

CComBSTR s1;
InnerMakeName(L"", &s1);

STDMETHODIMP CParent::InnerMakeName(BSTR input, BSTR* pVal)
{
    CString s(L"hello");
    return CComBSTR(s.AllocSysString()).CopyTo(pVal);
}

Fix solution to case 6:

CComBSTR s1;
InnerMakeName(L"", &s1);
STDMETHODIMP CBryan::InnerMakeName(BSTR input, BSTR* pVal)
{
    CString s(L"hello");
    *pVal = s.AllocSysString();
    return S_OK;
}

Case 7:

    class CSon
    {
        CComPtr<IParent> m_pParent;
        
        void setParent(IParent* pParent)
        {
            m_pParent = pParent;
        }
    }
    
    class CParent
    {
        CComPtr<IChild> m_pChild;
         
        STDMETHODIMP CParent::get_MySon(ISon** pVal)
        {
            if (m_pChild == NULL)
            {
                ObjectLock Lock(this);

                if (m_pChild == NULL)
                {
                    CComObject<CSon> *pDataElm;
                    CComObject<CSon>::CreateInstance(&pDataElm);    
                    CComPtr<ISon> ptr(pDataElm);
                    m_pChild = ptr;
                    pDataElm->setParent(this);
                }
            }
            return m_pChild.CopyTo(pVal);
        }
    }

Fix solution to Case 7:

class CSub
{
    CComPtr<IClass> m_pParent;
   
    void setParent(IClass* pParent)
    {
        m_pParent = pParent;
    }
}

Case 8:

CComBSTR bstrIndex(L"IndexValue");
CComVariant vtIndex;
vtIndex.vt = VT_BSTR  | VT_BYREF;
vtIndex.pbstrVal = &bstrIndex;
CComBSTR result;
get_Item(vtIndex, &result);

STDMETHODIMP CMyClass::get_Item(VARIANT v, BSTR* pVal) 
{
    SimplifyVar(&v);
    ....use v
    return S_OK;
}
HRESULT CMyClass::SimplifyVar(VARIANT *v)
{
    //case VT_BSTR:
    if (v->vt == ( VT_BSTR|VT_BYREF))
    {
        CComBSTR tmp = *(v->pbstrVal);
        VariantClear(v);
        v->vt=VT_BSTR;
        v->bstrVal = tmp.Detach();
        return S_OK; 
    }

    return S_OK;
}

Fix solution to case 8:

CComBSTR bstrIndex(L"IndexValue");
CComVariant vtIndex;
vtIndex.vt = VT_BSTR  | VT_BYREF;
vtIndex.pbstrVal = &bstrIndex;
CComBSTR result;
get_Item(vtIndex, &result);

STDMETHODIMP CMyClass::get_Item(VARIANT v, BSTR* pVal) 
{
    CComBSTR simpleIndex;
    SimplifyVar(&v, &simpleIndex);
    return S_OK;
}
HRESULT CMyClass::SimplifyVar(VARIANT *v, BSTR* pVal)
{
    //case VT_BSTR:
    if (v->vt == ( VT_BSTR|VT_BYREF))
    {
        CComBSTR tmp = *(v->pbstrVal);
    
        pVal = v->pbstrVal;
        return S_OK;
    }

    return S_OK;
}

完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容