目录
4.1、通过malloc/new等动态申请的内存,在使用完后,没有调用free/delete去释放(也可能是调用了上面讲到的HeapAlloc或VirtualAlloc等API接口)
4.2、函数调用者调用内部申请内存的接口,函数调用完成后,没有去释放接口内部动态申请的内存
4.3、在多态中,没有将基类的析构函数声明为虚函数,导致delete多态时的子类对象时没有执行子类的析构函数,导致内存泄漏
4.4、第三方注入库有内存泄漏,注入到我们的程序进程中导致我们的程序发生内存泄漏
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(已更新到400多篇,持续更新中...)
https://blog.csdn.net/chenlycly/category_11931267.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/124272585C++ 软件开发从入门到精通(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12695902.htmlC++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_2276111.html 内存泄漏是C++程序常见内存问题之一,在软件项目中时常会遇到。内存泄漏问题排查起来比较麻烦,有时需要尝试多种方法和工具,特别是大型软件项目,排查的难度会更大。本文根据多年的项目实践以及遇到的多个内存泄漏的问题案例及场景,对引发内存泄漏的原因以及排查方法做一个详细的总结,以供大家借鉴或参考。
1、概述
所谓内存泄漏,就是动态申请的内存在使用完后没有释放,导致了泄漏。有内存泄漏的代码如果被频繁地执行,则会导致频繁的内存泄漏,会导致程序占用的虚拟内存越来越多,程序运行可能会变慢(程序要频繁地在虚拟内存与物理内存之间切换)。当程序占用的虚拟内存接近或达到程序虚拟内存的上限时,就会因内存不足产生异常,程序紧接着就会发生崩溃或闪退。
一般程序的业务代码基本都是运行在用户态的,占用的主要是用户态的虚拟内存,所以内存泄漏一般都是用户态的内存泄漏。如果是驱动程序,则运行在内核态的。
程序中发生内存泄漏,且内存泄漏在持续的发生(发生内存泄漏的代码在频繁地执行),就会因内存不足产生异常,程序紧接着就会发生崩溃或闪退。。这种内存泄漏将是致命的,也是必须要解决的!
此外,内存泄漏具有一定的隐蔽性,直到程序发生闪退或崩溃时才能被发现。程序刚启动时可以正常运行,可能运行一段时间后程序就莫名其妙的闪退崩溃了。可能内存泄漏发生在某个代码块中,当执行该代码块的相关操作或者业务流程时才会触发内存泄漏,持续地执行该操作,才会导致程序的内存持续的增长。
2、内存泄漏与程序的位数
根据内存寻址的位数,程序一般分为32位和64位。64位程序是不能运行在32位系统中的,但32位程序是可以运行在64位系统中的(64位系统对32位程序保持兼容)。关于32位程序与64位程序的区别以及相关问题案例,不是此处讨论的重点,我就不再展开了,可以查看我之前写的文章:
32位程序在64位系统中运行需要注意的重定向问题https://blog.csdn.net/chenlycly/article/details/139564448应用程序无法正常启动(0xc000007b)问题的详细排查(文章第8节详细讲到32位程序与64位程序的区别)
https://blog.csdn.net/chenlycly/article/details/126298265 对于32位程序,系统在程序启动时会给程序进程分配4GB的虚拟内存(内存按32位寻址),在默认情况下,2GB是用户态的虚拟内存,2GB是内核态的虚拟内存。如果程序中内存泄漏的代码(用户态的泄漏)被频繁地执行,程序占用的用户态虚拟内存可能很快就能达到2GB的用户态虚拟内存的上限,从而会因为内存不足导致程序发生异常。当然,我们可以在Visual Studio中对exe主程序工程开启/LargeAddressAware大地址编译选项:
将32程序用户态虚拟内存从2GB扩大到3GB,虽然可以缓解内存泄漏的问题,但对于持续的泄漏也无济于事。
对于64位程序,内存寻址范围是64位,系统会分配比32位程序的4GB多的多的虚拟内存,但即便再大,也是有上限的,如果持续泄漏,也总会到耗尽的那一刻的,程序终归会因为内存不足发生崩溃闪退。
至于内存不足后程序为什么会发生崩溃或闪退,下面会详细讲解,此处就不展开了。
3、调用哪些接口去动态申请内存?
C++代码中主要使用操作符new(要用delete去释放)或者C函数malloc(要用free去释放)去动态申请内存。在Windows C++程序中,也可以调用Windows系统API函数HeapAlloc从堆上分配内存(要用HeapFree去释放),还可能调用API函数VirtualAlloc或VirtualAllocEx从虚拟内存上分配内存(要用VirtualFree或VirtualFreeEx去释放),还可能调用其他的API函数。
调用这些操作符或函数去动态申请内存时,如果在内存使用完后不去调用对应的接口去释放内存,则会导致内存泄漏。注意,使用不同操作符或者函数去动态申请内存,要用对应的操作符或者函数去释放,不能交叉使用,胡乱调用接口可能会引发异常崩溃。
有时我们使用new或malloc去申请一段较大的连续内存时,可能会因为内存紧张而申请不到,而使用VirtualAlloc或VirtualAllocEx申请到的几率要大一些,这点我们在项目中使用过。
之前排查的一个WebRTC开源库中的内存泄漏问题,就是没调用HeapFree去释放使用HeapAlloc申请来的堆内存,导致的内存泄漏。可以去查看对应的文章:
开源WebRTC库放大器模式在采集桌面图像时遇到的DPI缩放与内存泄漏问题排查https://blog.csdn.net/chenlycly/article/details/131146506 有一个
典型的C++面试题,问new/delete与malloc/free的区别,可以查看我的文章:
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到510多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏3:(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,专栏文章已经更新到400多篇,持续更新中...)
C/C++实战进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、C++11及以上新特性(不仅看开源代码会用到,日常编码中也会用到部分新特性,面试时也会涉及到)、常用C++开源库的介绍与使用、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(排查软件异常的手段与方法、分析C++软件异常的基础知识、常用软件分析工具使用、实战问题分析案例等)、设计模式、网络基础知识与网络问题分析进阶内容等。
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
4、引发内存泄漏的常见原因总结
根据多年的项目实践以及遇到的多个内存泄漏的问题案例及场景,总结出引发内存泄漏的常见原因主要有以下几种。
4.1、通过malloc/new等动态申请的内存,在使用完后,没有调用free/delete去释放(也可能是调用了上面讲到的HeapAlloc或VirtualAlloc等API接口)
可能是新人写的代码,因为考虑问题不全面,压根就没想去释放内存。再者,可能是业务逻辑控制的有问题导致的,动态申请的内存是给软件业务模块用的,受业务逻辑控制的,使用完成后应该要将内存释放掉,释放的时机也是根据业务要求去控制的。如果控制的不好,就执行不到释放内存的代码。也有可能是代码业务逻辑有调整,因为有部分代码点没有同步修改,导致内存没有释放,比如我前段时间排查的内存泄漏:
使用历史版本比对法排查C++程序中的内存泄漏问题https://blog.csdn.net/chenlycly/article/details/141002375
4.2、函数调用者调用内部申请内存的接口,函数调用完成后,没有去释放接口内部动态申请的内存
调用某些特殊的接口,接口内部动态申请内存,然后将地址返回给接口调用者使用,调用者在使用完后需要将对应的内存释放掉,如果没释放,则会造成内存泄漏。
比如我们在调用GDI+库中的Bitmap静态成员函数FromFile、FromStream和FromHBITMAP等静态接口时,这些接口内部都会new出一个Bitmap对象,然后把new出的Bitmap对象地址作为返回值,返给外部使用,如下所示:
// 头文件中函数的声明
static Bitmap* FromFile(
IN const WCHAR *filename,
IN BOOL useEmbeddedColorManagement = FALSE
);
static Bitmap* FromStream(
IN IStream *stream,
IN BOOL useEmbeddedColorManagement = FALSE
);
static Bitmap* FromHBITMAP(IN HBITMAP hbm,
IN HPALETTE hpal);
// cpp文件中函数的实现
inline Bitmap*
Bitmap::FromFile(
IN const WCHAR *filename,
IN BOOL useEmbeddedColorManagement
)
{
return new Bitmap(
filename,
useEmbeddedColorManagement
);
}
inline Bitmap*
Bitmap::FromStream(
IN IStream *stream,
IN BOOL useEmbeddedColorManagement
)
{
return new Bitmap(
stream,
useEmbeddedColorManagement
);
}
inline Bitmap*
Bitmap::FromHBITMAP(
IN HBITMAP hbm,
IN HPALETTE hpal
)
{
return new Bitmap(hbm, hpal);
}
上述函数不会去管理这个返回的Bitmap对象的生命周期(从对象创建到对象销毁),因为是给外部使用的,外部根据自己的需要确定使用到什么时候,并且外部负责在不用的使用去delete这个对象,如下所示:
Bitmap *pSrcBmp = Bitmap::FromHBITMAP(hBmp, NULL);
if (pSrcBmp == NULL)
{
return;
}
// ... 中间代码省略
delete pSrcBmp;
如果不去delete,则会导致内存泄漏。
4.3、在多态中,没有将基类的析构函数声明为虚函数,导致delete多态时的子类对象时没有执行子类的析构函数,导致内存泄漏
这个场景有较强的隐蔽性。在多态中,将子类对象地址赋值给父类的指针,我们在delete该父类指针时,因为指针中存放的是子类的对象,应该要执行子类的析构函数,析构函数除了释放子类对象本身的内存,可能在析构中还会执行其他资源的释放,如果父类的析构函数不声明为虚函数,则这种场景下子类的析构就不会被执行到,就会导致内存泄漏。这个场景的示例代码如下:
// 基类
class CBase
{
public:
CBase();
~CBase();
};
// 派生类
class CDerived
{
public:
CDerived();
~CDerived();
};
// 多态
CBase* pBase = new CDerived;
delete pBase;
new出一个子类CDerived对象,赋值给父类的指针CBase* pBase,然后再delete pBase时因为父类CBase的析构没有声明为虚函数,导致没有执行子类CDerived的析构函数,从而导致内存泄漏。
有时,子类的析构函数不仅要析构子类对象的内存,可能还会在析构函数中去释放其他的资源,比如我们在子类对象中定义了一个STL列表vector<CChatDlg*> m_vtChatDlgList,在子类的析构函数中会去for循环遍历这个列表去delete列表中的CChatDlg对象,如下所示:
class CDerived
{
public:
CDerived();
~CDerived();
private:
vector<CChatDlg*> m_vtChatDlgList;
};
CDerived::~CDerived()
{
for ( int i = 0; i < m_vtChatDlgList.size(); i++ )
{
CChatDlg* pChatDlg = m_vtChatDlgList[i];
delete pChatDlg;
}
}
如果子类的析构函数没执行到,则不仅子类对象本身的内存没释放,而且析构函数中释放STL列表中存放的类对象也没有释放,这些都是内存泄漏。
到底什么时候该将析构函数声明为虚函数?什么时候不要将析构函数声明为虚函数呢?
如果将C++类的析构函数声明为虚函数,C++类对象中会内置一个虚函数表指针,还会维护一个虚函数表(存放虚函数代码段的地址),这是声明为虚函数的开销。如果类可能会被继承,则最好将其析构函数声明为虚函数。如果类不会被继承,且明确使用了关键字final标识(使用该关键字就设定该类不能被继承,这是C++11新增的关键字),就不要将析构函数设置为虚函数。
关于C++11新增的几个常用关键字的说明,可以查看我的文章:
C++11 新特性 ① | C++11 常用关键字实战详解
https://blog.csdn.net/chenlycly/article/details/132701306
4.4、第三方注入库有内存泄漏,注入到我们的程序进程中导致我们的程序发生内存泄漏
这是我们在项目中遇到的真实案例,客户系统中安装了第三方安全软件,安全软件基本都是通过远程注入到各个进程的方式实现对各个进程实时监控。结果这个注入模块在某个场景下有内存泄漏,因为其注入到我们的程序进程中,位于我们的进程空间中,占用的是我们程序进程的虚拟内存,这个注入模块有泄漏,就是我们整个进程有泄漏,对我们的程序产生直接的影响。
当时我们的软件在客户的机器上运行半个小时后,就会出现闪退崩溃,后来我们发现程序的内存在运行过程中在不断的增长,判断是程序中发生了内存泄漏。客户一开始不认为是第三方安全软件的问题,因为其他软件在该问题电脑上运行并没有明显的问题。后来我们使用内存分析工具定位到泄漏发生在第三方安全软件的注入模块中,客户才愿意联系第三方安全软件去排查这个泄漏问题。
5、内存泄漏问题的排查
首先我们要确定程序中发生了内存泄漏,然后使用一些分析工具去分析,找出内存泄漏的代码点,进而解决问题。
5.1、发生内存泄漏的程序为何会发生闪退崩溃?
往往在程序运行发生异常后,经过观察分析才会发现程序中发生了内存泄漏,是内存泄漏引发的问题。
程序运行过程中遇到的各种问题,基本都是后知后觉,都是产生了异常的现象后,才知道程序发生了异常,才去排查引发异常的原因!内存泄漏问题也是如此,也是后知后觉,通过现象推测分析程序中可能发生了内存泄漏。
程序中发生持续的内存泄漏,程序的用户态虚拟内存就会逐渐地接近系统给程序进程分配的用户态虚拟内存的上限(比如32位程序,默认的用户态虚拟内存的上限是2GB),此时再使用malloc或new去动态申请内存时可能就会申请失败,导致程序产生异常,我们在项目中已多次遇到过。
我们在项目中主要遇到以下两类场景:
1)内存不足,malloc申请内存失败,返回NULL
这个场景发生在WebRTC开源库中,开源库内部因为业务的需要,调用malloc动态申请内存。因为程序占用的虚拟内存接近用户态的虚拟内存的上限,没有多余的内存可供分配了,所以malloc申请内存失败,返回NULL。
WebRTC开源库内部的代码判断出malloc返回NULL,认为这个是fatal致命的(因为申请不到内存,业务没法正常的展开,WebRTC内部认为这是致命的),直接调用abort强行将当前进程终止了!其实此时并没有产生异常,是调用abort强行终止进程的,程序直接闪退消失了,给人感觉是程序崩溃了!这个项目问题案例,我之前专门写了文章,可以去查看文章:
WebRTC开源库内部调用abort函数引发C++程序发生闪退问题的详细排查https://blog.csdn.net/chenlycly/article/details/129460580 对于这种调用abort强行终止进程的问题,还有一个细节值得注意一下。如果程序中安装了异常捕获,此种场景下是不会生成dump文件的。因为当前是WebRTC开源库内部检测到malloc返回NULL,直接调用abort,程序并没有发生C++上的异常,所以异常捕获是感知不到的(发生C++上的异常,异常捕获模块才能感知到),虽然给人一种程序发生崩溃闪退的感觉,但不会生成dump文件的。
2)内存不足,new失败抛出异常
当程序内存不足时,使用new去动态申请内存失败,默认情况下会抛出bad_alloc异常。这种场景是产生了异常,如果程序中安装了异常捕获模块,且异常捕获模块感知到了,是会生成dump文件的。比如我们有次遇到的因为内存泄漏导致内存不足,导致new抛出了异常:(下图中抛出bad_alloc异常的代码就是new那句代码)
抛出的是std::bad_alloc异常,就是我们讲的bad_alloc异常,导致程序发生崩溃闪退(代码中没有对这个bad_alloc异常进行处理)。如果dump文件中显示new时抛出了bad_alloc异常,可以怀疑程序中可能存在内存泄漏,然后使用工具观察程序运行过程中的内存变化,去确定是否真的是内存泄漏导致的!
这里还有一个细节,new失败抛出bad_alloc异常,不一定是内存不足导致的。也可能是程序中有堆内存越界,导致堆内存被破坏引发的。堆内存被破坏,会导致程序导出胡乱的崩溃,一会再new的地方,一会在delete的地方。
5.2、如何确定当前程序发生了内存泄漏?
如果根据问题现象或者初步分析,怀疑当前问题可能是内存泄漏导致的,我还需要观察程序运行时或者执行某个指定的操作时占用的内存是否在持续的增加。
可以在Windows任务管理器中找到程序进程,然后到程序中执行引发内存泄漏的操作,再到任务管理中查看进程占用的内存变化:
但任务管理器中只能看到专用工作集、共享工作集、提交大小等内存信息,但这些内存不是程序进程占用的总的虚拟内存,一般我们需要查看程序占用的总的虚拟内存,最为直接,也最为直观!
要查看程序占用的总虚拟内存,可以使用Process Explorer工具。Process Explorer在默认情况下是不显示虚拟内存列的,在进程列表的标题栏中右键点击,弹出右键菜单,点击“Select Columns”,在弹出的窗口中切换到“Process Memory”标签页下,将“Virtual Size”勾选上:
进程列表中就会显示虚拟内存占用了:
我们一般讲的内存不足,指的是虚拟内存的不足,所以需要查看程序进程的虚拟内存,使用Process Explorer查看程序进程的虚拟内存的变化,就可以确定是否在持续增长了。
此外,也可以查看执行某个操作时内存泄漏了多少,具体的办法是,记录一下操作前的程序的虚拟内存,然后操作后再记录一下虚拟内存,两次虚拟内存之差,就是此次操作泄漏的内存大小!
5.3、使用工具排查内存泄漏
内存泄漏问题排查起来也比较复杂,有时需要尝试多种方法和工具,特别是代码量大的大型软件项目,排查的难度会更大。
一般我们要使用一些调试器或专用的内存分析工具来排查。在Windows中,可以使用Windbg(调试器)、Visual Leak Detector(VLD)、VMMap和DebugDiag等。在Linux系统中,则可以使用内存专用检测工具Valgrind和AddressSanitizer。
有时内存泄漏点会定位不准,代码中包含了上百个业务模块,代码中到处都是动态new内存的,专业的内存检测工具可能也无法界定是哪块代码有泄漏泄露。所以可能需要尝试使用多个内存检测工具,但即便使用多个工具后可能也无法定位内存泄露点。
关于VLD检测内存泄漏的完整案例,可以查看我的文章:
使用Visual Leak Detector(VLD)排查C++程序内存泄漏https://blog.csdn.net/chenlycly/article/details/135472681 关于Windbg检测内存泄漏的案例,可以查看我的文章:
使用Windbg排查C++程序内存泄漏问题https://blog.csdn.net/chenlycly/article/details/121295720使用 Windbg 的 !heap 命令分析内存泄漏
https://blog.csdn.net/chenlycly/article/details/131576063 关于Linux系统上的Valgrind和AddressSanitizer工具,可以查看我的文章:
Windows和Linux下排查C++软件异常的常用调试器与内存检测工具详细介绍https://blog.csdn.net/chenlycly/article/details/126381865为什么选择C/C++内存检测工具AddressSanitizer?如何使用AddressSanitizer?
https://blog.csdn.net/chenlycly/article/details/132863447 AddressSanitizer工具原先只支持Linux,现在也可以在Windows上使用了。微软在Visual Studio 2019的16.9版本们引入了AddressSanitizer,在安装Visual Studio 2019的16.9版本及以后的版本时,会默认安装AddressSanitizer工具:(默认勾选“C++ AddressSanitizer”)
对于如何在VS中如何使用AddressSanitizer内存分析工具,可以看一下微软官方文章的详细说明:
在Visual Studio中使用AddressSanitizerhttps://docs.microsoft.com/zh-cn/cpp/sanitizers/asan?view=msvc-170此处我就不详细展开了,大家需要使用的话,可以去详细研究一下。
有时需要尝试使用多个内存检测工具去排查,之前记录了内存泄漏排查的专题,感兴趣也可以去看看:
C++内存泄漏排查专题https://blog.csdn.net/chenlycly/category_12370029.html
6、最后
本文系统总结了引发C++软件内存泄漏的常见原因和场景,给出了排查内存泄漏的若干工具和方法,并给出使用这些工具分析内存泄漏的实战问题实例,希望能给大家提供一定的参考。