最近花了很多时间解决一个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,可以参考 link 和 link.
还有一个情况,在函数调用的时候,参数会被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;
}
完。