文章目录
前言- 每次游戏上线前跑压力测试,总会发现一些内存泄漏,而且由于项目庞大,添加上检测工具以后,服务器运行就变得奇慢无比,非常耗时,所以有必要总结一下其中的一些原因和解决方案,方便日后做自动化。
一、准备工作
1、工具安装
-
内存泄漏检测工具:Visual Leak Detector
-
链接: https://pan.baidu.com/s/1fBPiQ-N5C0lLAl-y1MP43Q 提取码: a5bn
-
附带调试代码:
-
链接: https://pan.baidu.com/s/1a_zPcncK3gtrCIVB2Q3epw 提取码: ppk2
2、目录添加
- a)vld 头文件目录添加到 vs(头文件目录在vld安装目录下)
- b)vld 库文件目录添加到 vs(库文件目录在vld在安装目录下)
3、信息配置
- a)工程配置必须为 Debug 模式;
- b)将 VLD 工具安装目录下的 vld.ini 文件拷贝到需要检测的程序的工程代码(或者可执行文件)目录下,然后对配置进行一定的修改,这样就能将最后的报告输出到文件中:
ReportFile =.\memory_leak_report.txt
ReportTo = both
- c)对需要检测的代码增加一行头文件包含,放在最前面即可;
#include <vld.h>
二、基础测试
1、简单尝试
- c++的内存泄漏最原生的原因就是分配的内存没有释放,即 new/malloc 出的内存没有与之配对的 delete/free,举个最简单的例子;
#include "vld.h"
int main() {
int *p = new int;
*p = 0x09ABCDEF;
return 0;
}
- 程序退出的时候会进行内存泄漏的检测,并且输出报告;
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 1 at 0x0000000013065440: 4 bytes ----------
Leak Hash: 0xD58AB4EF, Count: 1, Total 4 bytes
Call Stack (TID 17172):
MSVCR120D.dll!operator new()
e:\dreamrivakes\trunk\program\other\memoryleaktester\memoryleaktester\memoryleaktester\main.cpp (78): MemoryLeakTester.exe!main() + 0xA bytes
f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c (466): MemoryLeakTester.exe!mainCRTStartup()
KERNEL32.DLL!BaseThreadInitThunk() + 0x14 bytes
ntdll.dll!RtlUserThreadStart() + 0x21 bytes
Data:
EF CD AB 09 ........ ........
Visual Leak Detector detected 1 memory leak (56 bytes).
2、报告分析
- 报告中包含了几个内容:
-
- 【Block X at …: Y bytes】第 X 个块(new)泄漏了 Y 个字节;
-
- 【Call Stack】泄漏内存的堆栈信息,双击对应的行可以跳到具体代码位置;
-
- 【Data】泄漏内存的数据信息;
-
- 【Visual Leak Detector detected X memory leak (Y bytes).】总共 X 处泄漏,共泄漏字节数 Y 字节;
3、某些疑惑
- 观察代码只泄漏了 4 个字节(1个int),但是报告显示泄漏了 56 个字节,多了 52 个字节,所以再做一些尝试,尝试如下;
#include "vld.h"
int main() {
char *p1 = new char;
*p1 = 0xAB;
short *p2 = new short;
*p2 = 0xABCD;
int *p3 = new int;
*p3 = 0x09ABCDEF;
long long *p4 = new long long;
return 0;
}
- 得到的报告如下:
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 1 at 0x00000000E1167080: 1 bytes ----------
Leak Hash: 0xE21F7247, Count: 1, Total 1 bytes
Call Stack (TID 16576):
...
Data:
AB
---------- Block 2 at 0x00000000E1165BF0: 2 bytes ----------
Leak Hash: 0xE58559DC, Count: 1, Total 2 bytes
Call Stack (TID 16576):
...
Data:
CD AB
---------- Block 3 at 0x00000000E1165C60: 4 bytes ----------
Leak Hash: 0xB0A3AA1A, Count: 1, Total 4 bytes
Call Stack (TID 16576):
...
Data:
EF CD AB 09
---------- Block 4 at 0x00000000E1165440: 8 bytes ----------
Leak Hash: 0x49D5C84C, Count: 1, Total 8 bytes
Call Stack (TID 16576):
...
Data:
CD CD CD CD CD CD CD CD
Visual Leak Detector detected 4 memory leaks (223 bytes).
- i. char、short、int、long long 分别泄漏 1、2、4、8 个字节,但是最后的泄漏的字节数不是 15 而是 223 ???
- ii. 观察发现:
(223 - 15) / 4 = 52
- iii. 所以大胆猜测,每个 new 会导致对应的内存块多泄漏 52 个字节,这个可能是 VLD 工具泄漏的,猜测是它要记录内存泄漏的信息,所以这些信息不能在程序退出的时候释放(有兴趣可以看下 VLD 源码),但是这个不影响我们查内存泄漏的问题,只要没有泄漏,这 52 个字节也不会泄漏;
4、解决泄漏
- 解决内存泄漏,就是让所有的 new 匹配 delete;
#include "vld.h"
int main() {
int *p = new int;
delete p;
return 0;
}
No memory leaks detected.
Visual Leak Detector is now exiting.
三、常见内存泄漏
1、虚析构
- 面试 c++ 的时候总会听到一些熟悉的问题:
i. 为什么基类对象的析构函数一般都要声明 virtual 关键字?
ii. 虚析构的作用和原理是什么?
- 来看一段代码:
#include "vld.h"
using namespace std;
// 基类
class LeakBaseObject {
public:
LeakBaseObject() {
printf("LeakBaseObject Construction!\n");
}
~LeakBaseObject() {
printf("LeakBaseObject Destruction!\n");
}
};
// 派生类
class LeakObject : public LeakBaseObject {
public:
LeakObject() {
printf("LeakObject Construction!\n");
p = new int[100];
}
~LeakObject() {
printf("LeakObject Destruction!\n");
if (p) {
delete[] p;
p = nullptr;
}
}
private:
int *p;
};
int main() {
// 基类指针指向派生类
LeakBaseObject *pLBObj = new LeakObject();
delete pLBObj;
}
- 来看下控制台输出:
LeakBaseObject Construction!
LeakObject Construction!
LeakBaseObject Destruction!
- 派生类对象在 new 的时候,会调用基类的构造函数,再调用自身的构造函数;
- 当基类指针指向派生类对象,并且进行 delete 的时候,只会调用基类的析构函数,不会调用派生类的析构函数,原因是因为基类的析构函数不是虚的,所以这里只要派生类的成员变量中有进行 new 操作申请内存,并且在析构函数里面 delete 的,这里就有可能产生内存泄漏;
- 泄漏字节数 = 100 * 4 + 52 = 452 字节;
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 2 at 0x000000004671BB20: 400 bytes ----------
Leak Hash: 0x1B693661, Count: 1, Total 400 bytes
Call Stack (TID 8688):
... ...
Data:
... ...
Visual Leak Detector detected 1 memory leak (452 bytes).
- 解决方案只要在基类的 ~LeakBaseObject() 函数前加上 virtual 关键字即可;
class LeakBaseObject {
public:
LeakBaseObject() {
printf("LeakBaseObject Construction!\n");
}
virtual ~LeakBaseObject() {
printf("LeakBaseObject Destruction!\n");
}
};
2、STL容器泄漏
- 所有STL容器的内存分配都是动态分配的,也就是在声明的时候,其实已经做了内存分配,还是拿虚析构来举例;
- 以下这段代码和上面那段代码的不同之处只有派生类的成员变量这里,从 int 指针变成了 vector;
#include "vld.h"
using namespace std;
// 基类
class LeakBaseObject {
public:
LeakBaseObject() {
printf("LeakBaseObject Construction!\n");
}
~LeakBaseObject() {
printf("LeakBaseObject Destruction!\n");
}
};
// 派生类
class LeakObject : public LeakBaseObject {
public:
LeakObject() {
printf("LeakObject Construction!\n");
}
~LeakObject() {
printf("LeakObject Destruction!\n");
}
private:
vector<int> m_kVector;
};
int main() {
// 基类指针指向派生类
LeakBaseObject *pLBObj = new LeakObject();
delete pLBObj;
}
- 运行结果如下:
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 2 at 0x00000000781758C0: 16 bytes ----------
Leak Hash: 0x211451AF, Count: 1, Total 16 bytes
Call Stack (TID 20216):
MSVCR120D.dll!operator new()
d:\microsoft visual studio 12.0\vc\include\xmemory0 (848): MemoryLeakTester.exe!std::_Wrap_alloc<std::allocator<std::_Container_proxy> >::allocate()
d:\microsoft visual studio 12.0\vc\include\vector (624): MemoryLeakTester.exe!std::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > >::_Alloc_proxy() + 0xF bytes
d:\microsoft visual studio 12.0\vc\include\vector (603): MemoryLeakTester.exe!std::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > >::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > >() + 0xA bytes
d:\microsoft visual studio 12.0\vc\include\vector (681): MemoryLeakTester.exe!std::vector<int,std::allocator<int> >::vector<int,std::allocator<int> >()
e:\dreamrivakes\trunk\program\other\memoryleaktester\memoryleaktester\memoryleaktester\main.cpp (25): MemoryLeakTester.exe!LeakObject::LeakObject()
e:\dreamrivakes\trunk\program\other\memoryleaktester\memoryleaktester\memoryleaktester\main.cpp (64): MemoryLeakTester.exe!main() + 0x30 bytes
f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c (466): MemoryLeakTester.exe!mainCRTStartup()
KERNEL32.DLL!BaseThreadInitThunk() + 0x14 bytes
ntdll.dll!RtlUserThreadStart() + 0x21 bytes
Data:
...
Visual Leak Detector detected 1 memory leak (68 bytes).
- 不出所料,泄漏字节数为 68 = 16 + 52;
- vector 也是一个类,也有它的构造函数,构造过程中也会进行对应的内存分配,具体堆栈如下:
- 跟进到 STL 底层源码,_Allocate 的函数实现如下:
template<class _Ty> inline //i
_Ty *_Allocate(size_t _Count, _Ty *)
{
// allocate storage for _Count elements of type _Ty
void *_Ptr = 0;
if (_Count == 0)
;
else if (((size_t)(-1) / sizeof (_Ty) < _Count) // ii
|| (_Ptr = ::operator new(_Count * sizeof (_Ty))) == 0) // iii
_Xbad_alloc(); // report no memory
return ((_Ty *)_Ptr);
}
- i. 这是一个模板函数,_Ty 代表具体的类型;
- ii. 64位程序的 size_t 是 unsigned long long,所以 -1 转换成无符号整数,是 64位无符号整数的最大值,这步判断主要时对最大内存申请进行限制;
- iii. ::operator new 就是 new 关键字,构造函数里 vector 会初始化 _Count 个元素数据;
- 再去看 vector 的析构函数,会对这部分 new 出来的内存进行释放,所以如果派生类的析构函数没有调用到的话, vector 的析构函数也不会调用,就会产生内存泄漏了;
3、Protobuf的内存泄漏
- 注意进程退出前,需要调用一下 pb 的静态函数 :
google::protobuf::ShutdownProtobufLibrary();
- 否则,会产生内存泄漏,一般不调用也没事,进程关闭自动会释放,只是要查问题,所以还是调用一下,这里释放的是 Protobuf 内部的反射对象;