最近遇到一个C++工程内存泄漏的问题,经过排查,发现原来是 map 的使用有问题,本文记录了排查的过程,并给出一个类似的工程代码。
起因
某日,运维反馈生产环境某台设备出现问题,经组长排查,有两个工程服务占用内存较多,出现 OOM 被 Linux 系统干掉了。其中一个是我接手的工程,竟达到了 6GB,随即安排我排查。
排查
首先在本地虚拟机用 cppcheck、valgrind 测试,但没有发现容易看得懂的问题点,像 cppcheck 提示了很多不怎么要紧的问题——其实有大半问题已经在前两个月修正了。而 valgrind提示多的都是第三方库,比如 curl、xml、ssl 等。
因为没有头绪,也不敢随便动生产环境,所以写了个简单的 shell 脚本,用于监控程序的内存使用情况,并放在生产环境上,观察半天,发现隔1分钟就有少量内存泄漏,大概几十 KB 左右。因此得到存在内存泄漏的结论,但这只是验证猜测而已,因为在问题发现之初就已经把问题引致这方面了。
由于代码年代久远,错综复杂,几天过去也没头绪,还好发现概率比较小,还有时间排查。
后经同事指点,将监控程序频率提高,输出内存的同时打印日期时间,将其与工程日志的日期对比,缩小可疑范围,最后定位到传输模块的一个函数。
该函数使用 malloc 根据某个数据表名称为一个结构体变量指针申请内存,再放到 map 全局变量中,由于外部函数使用到,故不能释放,跟踪发现在类的析构函数中会释放内存,但在程序运行过程并没有进行析构,所以一直没有释放内存。存放到 map 的目的是防止多次申请内存,因为数据表的数量有限——不到十个,因此使用 map,在申请之前会查找 map,如不存在再申请,并存起来。
业务逻辑上并无问题,后在某个不起眼的地方看到了对该 map 变量的清除操作,即调用 clear 函数。怀疑此函数使用有误,于是写了一个简单的测试程序重现问题。最终得到结论:调用 map 的 clear 函数会清除 key,但如果 key 为指针,则不会释放其指向的内存。这正是问题根本原因所在。
重现问题
用于重现问题的测试程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| #include <stdio.h> #include <stdlib.h> #include <unistd.h>
#include <string> #include <map>
typedef struct { char Name[1024]; char Name1[1024]; char Name3[1024]; } TTableStruct;
class CMapLeak { public: CMapLeak(); ~CMapLeak(); TTableStruct *GetTable(const char *TableName); void TableTest();
private: std::map < std::string, TTableStruct * >m_mTable; };
CMapLeak::CMapLeak() { }
CMapLeak::~CMapLeak() { std::map < std::string, TTableStruct * >::iterator iter;
for (iter = m_mTable.begin(); iter != m_mTable.end(); iter++) { TTableStruct *pStruct = iter->second; if (pStruct != NULL) { delete pStruct; } } }
TTableStruct* CMapLeak::GetTable(const char *TableName) { TTableStruct *pStruct = NULL; std::map < std::string, TTableStruct * >::iterator iter; iter = m_mTable.find(TableName); if (iter == m_mTable.end()) { pStruct = new TTableStruct[100]; m_mTable[TableName] = pStruct; printf("NEW!!! struct ptr: %p\n", pStruct); } else { pStruct = iter->second; printf("struct ptr: %p\n", pStruct); } return pStruct; }
void CMapLeak::TableTest() { TTableStruct *pStruct = NULL; int i = 0; char tablename[32] = {0}; while (1) { sprintf(tablename, "table_%d", (i++)&0x03); pStruct = GetTable(tablename); printf("%s: struct ptr: %p\n", tablename, pStruct); printf("----------------\n"); sleep(1); } }
int main(void) { CMapLeak* pLeak = new CMapLeak(); pLeak->TableTest();
return 0; }
|
代码逻辑比较简单,为模拟生产环境的运行,直接使用死循环执行。先查找 m_mTable,如果 key 不存在则申请内存,否则直接返回已申请的内存。为了方便观察内存使用情况,在结构体中多加了几个数组。
当对 map 进行 clear 操作时,出现内存泄漏,监控脚本输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 有内存泄漏的: 23:14:31 dataserver ps mem: 13596 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413468 kB Cached: 637216 kB ------------- 23:14:36 dataserver ps mem: 15116 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2412884 kB Cached: 637216 kB ------------- 23:14:41 dataserver ps mem: 16636 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413492 kB Cached: 637216 kB ------------- 23:14:46 dataserver ps mem: 18460 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413144 kB Cached: 637216 kB ------------- 23:14:52 dataserver ps mem: 19980 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2412740 kB Cached: 637216 kB ------------- 23:14:57 dataserver ps mem: 21500 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413104 kB Cached: 637216 kB ------------- 23:15:02 dataserver ps mem: 23020 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413364 kB Cached: 637216 kB -------------
|
如果不调用 clear 函数,则内存占用较稳定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 23:10:12 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413616 kB Cached: 637212 kB ------------- 23:10:17 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2412888 kB Cached: 637212 kB ------------- 23:10:22 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413504 kB Cached: 637212 kB ------------- 23:10:27 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413092 kB Cached: 637212 kB ------------- 23:10:32 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413536 kB Cached: 637212 kB ------------- 23:10:37 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2412988 kB Cached: 637212 kB ------------- 23:10:43 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413348 kB Cached: 637212 kB ------------- 23:10:48 dataserver ps mem: 13900 VmRSS: 1068 kB System memory info: MemTotal: 3861496 kB MemFree: 2413796 kB Cached: 637212 kB -------------
|
小结
就目前排查结果看,只需要将原工程清除 map 的 clear 函数去掉即可,但排查过程,还是花了一定的时间。对于手动申请的内存,一定要十分留意其申请释放的操作,必须配对,否则会产生隐藏。