一般来讲,在C++项目里面,秉承避免直接使用new/delete来管理内存的规范,改用智能指针,能够避免很多内存泄漏的问题。但是总所周知,即使使用了shared_ptr也还是有可能产生内存泄漏,这就是老生常谈的循环引用了

一般的博客里讲到循环引用,会具这样的例子:

class Son;
class Father
{
public:
void Set(std::shared_ptr& son)
{
ref = son;
}
private:
std::shared_ptr ref;
};
class Son
{
public:
void SetFather(std::shared_ptr& father)
{
ref = father;
}
private:
std::shared_ptr ref;
};

这种常见的循环引用比较好发现,显然我们在编程的时候很明显的就能避免这种写法(除非是接手别人的代码)。更多的循环引用导致的泄漏,我认为是类似这种:

bool RtcSession::Start()
{
logtd("[%u] Start RtcSession", GetId());
std::shared_ptr session = GetSharedPtr(); //多线程下,经常出现这种代码
// SRTP srtp_transport_ = std::make_shared((uint32_t)SessionNodeType::Srtp, session);
// DTLS dtls_transport_ = std::make_shared((uint32_t)SessionNodeType::Dtls, session);
dtls_transport_->SetLocalCertificate(Application::Instance()->GetCertificate());
dtls_transport_->StartDTLS();
// ICE-DTLS dtls_ice_transport_ = std::make_shared((uint32_t)SessionNodeType::Ice, session, ice_port_);
auto offer_media_desc_list = offer_sdp_->GetMediaList();
auto peer_media_desc_list = peer_sdp_->GetMediaList();
if (offer_media_desc_list.size() != peer_media_desc_list.size())
{
return false;
}
for (size_t i = 0; i < peer_media_desc_list.size(); i++)
{
auto peer_media_desc = peer_media_desc_list[i];
auto offer_media_desc = offer_media_desc_list[i];
auto payload = peer_media_desc->GetFirstPayload();
if (payload == nullptr)
{
return false;
}
auto rtp_rtcp = std::make_shared(payload->GetId(), session,
peer_media_desc->GetMediaType() == MediaDescription::MediaType::Audio);
rtp_rtcp->Initialize();
rtp_rtcp->SetLocalSSRC(offer_media_desc->GetSsrc());
rtp_rtcp->SetPeerSSRC(peer_media_desc->GetSsrc());
rtp_rtcp->SetPayloadType(payload->GetId());
rtp_rtcp->EnableFeedback(payload->IsRtcpFbEnabled(PayloadAttr::RtcpFbType::TransportCc));
switch (peer_media_desc->GetDirection())
{
case MediaDescription::Direction::SendRecv:
rtp_rtcp->SetDirection(true, true);
break;
case MediaDescription::Direction::RecvOnly:
rtp_rtcp->SetDirection(true, false);
break;
case MediaDescription::Direction::SendOnly:
rtp_rtcp->SetDirection(false, true);
break;
case MediaDescription::Direction::Inactive:
rtp_rtcp->SetDirection(false, false);
break;
default:
rtp_rtcp->Stop();
break;
}
rtp_rtcp->RegisterUpperNode(nullptr);
rtp_rtcp->RegisterLowerNode(srtp_transport_);
rtp_rtcp->Start();
rtp_rtcp_map_[payload->GetId()] = std::move(rtp_rtcp);
}
srtp_transport_->RegisterUpperNode(nullptr); //这里协议栈上下节点相互引用 srtp_transport_->RegisterLowerNode(dtls_transport_);
srtp_transport_->Start();
dtls_transport_->RegisterUpperNode(srtp_transport_);
dtls_transport_->RegisterLowerNode(dtls_ice_transport_);
dtls_transport_->Start();
dtls_ice_transport_->RegisterUpperNode(dtls_transport_);
dtls_ice_transport_->RegisterLowerNode(nullptr);
dtls_ice_transport_->Start();
stop_ = false;
return Session::Start();
}

在多线程环境下,类成员注册了各种各样的回调,要维持上下文有效,就只能让成员持有类的智能指针,在效率上来看这个选择是没有错的,但是释放的时候就必须要小心。在上面一段代码里,self被下面各个成员持有,根本就进不去析构。如果类本身还持有了一些稀缺 资源如文件句柄等,那么很快整个系统就会出现异常。

如何排查这种不容易发现的内存泄漏?

排查首先要发现,发现内存泄漏很简单,在资源管理器上盯着,程序运行稳定之后内存不断上涨肯定有泄漏。有图形化工具的辅助会更方便。

发现了之后如何定位?这里就不建议直接看代码了,不好查,在linux下,直接上valgrind工具。

简单介绍一下valgrind,该工具可以检测下列与内存相关的问题 :未释放内存的使用

对释放后内存的读/写

对已分配内存块尾部的读/写

内存泄露

不匹配的使用malloc/new/new[] 和 free/delete/delete[]

重复释放内存

这里我们直接说内存泄漏检查,使用memcheck工具,这是我常用的命令:

valgrind --tool=memcheck --track-origins=yes --leak-check=full --show-leak-kinds=all --log-file=leak.txt --error-limit=no ./program

这里的./program是可执行文件

--log-file会生成一个报告文件,这个很重要,通常我们在linux跑服务程序,动辄上万行代码,靠终端输出是不行的。

这里要注意一点:valgrind只有在程序正常退出的时候才能完整生成内存使用报告。如果中途用kill之类的命令直接干掉后台进程,往往什么都没有。看一个示例结果:

可以很清楚看到,definitely lost说明必定存在泄漏的大小,只要解决这部分,其他的差不多可以忽略。

该报告按泄漏的内存大小排序,泄漏最大的怀疑块在最下面,直接向上看

valgrind已经把发生内存泄漏的堆栈位置标记出来了,接下来的工作就是去堆栈中的函数里分析代码,查看有么有循环引用或者未释放资源等操作。

还要注意的是,这么详细的堆栈需要代码用-g来编译,否则很难发现问题

至此可见,valgrind作为一种非侵入式的内存泄漏检查工具,是非常高效方便的。不像VLD,这个破玩意经常只打印泄漏的内存地址和大小,堆栈在哪里根本不清楚。