记一次查找内存泄露问题的过程

在最近的项目中,新接手了一个模块(以下简称A),在操作后退出,提示存在内存泄露,本文回顾解决这个内存泄露问题的全流程。

问题复现

为了定位内存泄露,对A模块的各个子功能都进行操作,然后退出程序,观察内存泄露报告,通过多次操作,总结出以下现象:

  • 有的子功能存在内存泄露,有的不存在。
  • 存在内存泄露的功能,每次泄露的内存块个数以及总大小是一样,平均在11KB左右。
  • 不同内存泄露的功能,泄露的内存块以及总量是不同的
  • 每个泄露的回溯堆栈都是一样的,定位到解析响应数据组件

通过以上现象,推测是解析响应数据时,存在内存泄露。

问题分析

数据解析模块是通用模块,查阅代码得知该模块根据请求Id,将响应数据解析成内部结构 CResponseData,并保存map中,抛开业务细节,保存和使用的逻辑如下:

// 存储响应数据
map<string, list<CResponseData*>> m_mapResp;

void ParseData(const char* pRespData, int nRespDataLen, CBaseAsk* pAsk)
{
	// xxx

	CResponseData* pRespData = new CResponseData(pRespData, nRespDataLen);
	
	// 从请求中获得唯一标识
	string strKey = pAsk->GetUniqueId();

	m_mapResp[strKey].push_back(pRespData);

	// xxx
}

CResponseData* GetRespData(const string& strReqKey)
{
	CResponseData* pData = nullptr;
	auto it = m_mapResp.find(strReqKey);
	if (it != m_mapResp.end())
	{
		pData = it->second.front();
		it->second.pop_front();
	}
	return pData;
}


数据解析模块内部通过 map<string, list<CResponseData*>> 结构保存解析完成的数据,通过 GetRespData 获取数据时,该接口要求外部使用者承担释放,响应数据是有解析模块负责申请。这里,违背了申请与释放一致的原则。

看到这里,起初猜测,有没有可能是两个同样的请求,后一个请求覆盖了前一个请求的数据,看到value是采用list保存,支持保存同个请求的多次响应数据,但这里还是存在依赖响应顺序的问题,不过,同个请求响应数据是幂等的,此处不影响。

在该模块的析构函数中,对list中所有尚未取走的响应数据进行统一释放。在调试过程,发现析构释放流程始终未进入,应该是都被取走了,接下来,大概率就是调用方的问题。

如果每个使用者按照接口约定,使用完后释放,不会出现泄漏。搜索了下调用方,有70多个,还有一个模块基于该接口再次封装,又有50多个调用方使用二次封装的接口。采用笨办法,一个一个的搜索排查。

经过漫长的排查,终于发现内存泄露的代码,大概长这个样子:

bool DealFun()
{
	// xxx

	CResponseData* pData = GetRespData(strReqKey);
	
	// 业务有效性校验
	if (xx)
	{
		// FreeData(pData);
	}

	// 很长的业务代码块
	

	// 在结尾处释放
	// FreeData(pData);

}

忘记释放的原因:

  1. 中间有多个退出路径时,遗漏了某些路径的释放
  2. 中间有很长的业务代码块,超过了一页屏幕,写到最后,忘记释放最初的数据指针。

数据解析模块是通用模块,几乎每个功能组件都依赖它,有的组件还会二次封装,自己内部使用,需要调用者释放的入口又多了。

问题解决

经过搜索排查,逐一补全遗漏释放的地方。

在排查过程中,发现一种较好应对这种场景的写法,核心思想是用do-while来包裹处理代码,通过break跳出,在最后处释放。具体实例如下:


bool DealFun()
{
	CResponseData* pData = GetRespData(strReqKey);
	do
	{
		// 业务有效性校验
		if (xx)
		{
			break;
		}
		
		/// 业务处理1

		/// 业务处理2

		/// 业务处理3

	}while(0);

	FreeData(pData);	
}

上述写法有以下好处:

  1. 有效性判断以及业务处理无需考虑释放,退出执行 break,在外部调用一次释放即可。
  2. 代码检查时,利用代码折叠,可把 do..while() 语句折叠起来,一眼看到结尾处的释放语句,直接明了,不用翻页查看

小结

此次内存泄露的原因是获取数据的接口要求调用方释放内存,但调用方忘记释放导致的。本文分享了一种较好应对这种场景的代码组织方法,

笔者推荐在响应处理中,将有效性校验以及业务处理封装成函数,减少主流程的代码长度。

posted @ 2022-02-18 16:47  浩天之家  阅读(118)  评论(0编辑  收藏  举报