目录
4.2、程序占用的用户态虚拟内存已经接近上限,申请不到可用的内存了
4.4、发生堆内存越界,导致堆内存被破坏,导致new操作产生异常
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(专栏文章已更新400多篇,持续更新中...)
https://blog.csdn.net/chenlycly/category_11931267.htmlC++ 软件开发从入门到精通(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)
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++软件开发维护的过程中,有时会遇到使用new或malloc动态申请内存失败的情况,导致软件出现异常。很多开发人员遇到这类问题可能会犯难,会很疑惑,搞不清楚是什么原因触发的。今天结合以往项目中遇到的多个问题实例,给大家详细总结一下引发动态申请内存失败的常见原因,以供借鉴或参考。
1、概述
C++程序在运行时所占用的内存,一般可分为栈内存区、堆内存区、全局/静态内存区、文字常量内存区及程序代码区5大分区。程序主要使用栈内存和堆内存,其中栈内存是与线程相关的,即创建线程时系统会给线程分配固定大小的栈内存(Windows系统中,默认分配1MB;Linux系统中,默认分配8MB),函数中的局部变量及函数调用时要传递的参数,它们的内存都是从所在线程的栈内存上分配的。所以栈内存的空间是有限的,程序中存放的大量数据主要存放在堆内存上,即使用new或malloc等动态申请来的堆内存。一般C++程序中,占用的堆内存要比占用的栈内存要大的多。
关于C++程序的内存分区,可以查看我的文章:
实例详解C++程序的五大内存分区https://blog.csdn.net/chenlycly/article/details/120958761 线程的栈空间是有限的,如果线程中调用的函数占用的栈内存之后达到所在线程的栈空间上限,则会导致Stack overflow线程栈溢出的异常,相关说明及案例,可以查看我的文章:
通过Stack Overflow线程栈溢出的问题实例,详解C++程序线程栈溢出的诸多细节https://blog.csdn.net/chenlycly/article/details/140908516 一般情况下,我们默认使用malloc或new动态申请内存都会成功的,但在个别异常的情况下,可能会出现申请失败的问题,比如程序中发生了内存泄漏导致程序剩余可用的内存不足、堆内存被破坏时会出现申请失败的场景。下面就来系统地总结一下动态申请内存的常见原因。
2、调用哪些接口去动态申请内存?
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
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到520多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到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++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
3、malloc和new申请内存失败说明
malloc是C语言的标准库函数(C运行时库),new则是C++的运算符,它们都可用于动态申请内存。下面我们来看看使用malloc和new申请内存失败时反应如何,是如何判断内存申请失败的。
下面我们假定程序中内存不足(可能是程序中发生了内存泄漏),导致malloc和new申请失败。下面详细看一下malloc和new失败时会有啥反应。
3.1、malloc申请内存失败,返回NULL
malloc申请内存失败时,会返回NULL,通过这个NULL返回值就可以判断内存申请失败了。
malloc申请内存失败,可能会引发程序发生闪退。之前我们在项目中就遇到过,当时程序中有内存泄漏,内存不够用了,导致WebRTC开源库内部代码在调用malloc申请内存时失败了,malloc返回NULL,WebRTC内部判断malloc返回NULL,认为这个是fatal致命的(因为申请不到内存,业务没法正常的展开,WebRTC内部认为这是致命的),直接调用abort强行将当前进程终止了,相关代码如下:
RTC_CHECK宏会校验malloc的返回值,如下:
如果malloc返回的地址为空,则会调用rtc_FatalMessage接口:
rtc_FatalMessage接口内部会调用FatalLog:
FatalLog接口中会在末尾处先是调用DebugBreak接口,如果当前程序正在调试,则DebugBreak会让调试器中断下来(从DebugBreak函数名就能看出这个函数的作用),目的是让正在调试的用户感知到!然后紧接着会调用abort接口,将当前进程强制终止掉!
其实abort接口内部也会让调试器中断下来,abort的内部实现代码如下所示:
/***
*void abort() - abort the current program by raising SIGABRT
*
*Purpose:
* print out an abort message and raise the SIGABRT signal. If the user
* hasn't defined an abort handler routine, terminate the program
* with exit status of 3 without cleaning up.
*
* Multi-thread version does not raise SIGABRT -- this isn't supported
* under multi-thread.
*******************************************************************************/
void __cdecl abort (
void
)
{
_PHNDLR sigabrt_act = SIG_DFL;
#ifdef _DEBUG
if (__abort_behavior & _WRITE_ABORT_MSG)
{
/* write the abort message */
_NMSG_WRITE(_RT_ABORT);
}
#endif /* _DEBUG */
/* Check if the user installed a handler for SIGABRT.
* We need to read the user handler atomically in the case
* another thread is aborting while we change the signal
* handler.
*/
sigabrt_act = __get_sigabrt();
if (sigabrt_act != SIG_DFL)
{
raise(SIGABRT);
}
/* If there is no user handler for SIGABRT or if the user
* handler returns, then exit from the program anyway
*/
if (__abort_behavior & _CALL_REPORTFAULT)
{
_call_reportfault(_CRT_DEBUGGER_ABORT, STATUS_FATAL_APP_EXIT, EXCEPTION_NONCONTINUABLE);
}
/* If we don't want to call ReportFault, then we call _exit(3), which is the
* same as invoking the default handler for SIGABRT
*/
_exit(3);
}
从上述代码可以看出,abort内部调用了raise(SIGABRT),该函数是触发一个SIGABRT信号终止异常,如果当前正在调试状态,会让调试器中断下来。最后调用了C函数_exit退出当前进程。
注意,上述问题中并没有产生C++异常,是调用abort强行终止进程的,程序直接闪退消失了,给人感觉是程序崩溃了,实际上并不是崩溃!这个项目问题案例,我之前专门写了文章,可以去查看文章:
WebRTC开源库内部调用abort函数引发C++程序发生闪退问题的详细排查https://blog.csdn.net/chenlycly/article/details/129460580 对于这种调用abort强行终止进程的问题,还有一个细节值得注意一下。如果程序中安装了异常捕获,此种场景下是不会生成dump文件的。因为当前是WebRTC开源库内部检测到malloc返回NULL,直接调用abort,程序并没有发生C++上的异常,所以异常捕获是感知不到的(发生C++上的异常,异常捕获模块才能感知到),虽然给人一种程序发生崩溃闪退的感觉,但不会生成dump文件的。
3.2、new申请内存失败,抛出异常
当程序内存不足时,使用new去动态申请内存失败,默认情况下会抛出bad_alloc异常。这种场景是产生了异常,如果程序中安装了异常捕获模块,且异常捕获模块感知到了,是会生成dump文件的。比如我们有次遇到的因为内存泄漏导致内存不足,导致new抛出了异常:
上图中使用Windbg打开dump文件,然后切换到异常上下文,查看异常发生时的函数调用堆栈抛出bad_alloc异常的代码就是new那句代码抛出的是std::bad_alloc异常,就是我们讲的bad_alloc异常,导致程序发生崩溃闪退(代码中没有对这个bad_alloc异常进行处理)。
如果dump文件中显示new时抛出了bad_alloc异常,可能是内存不足导致的,可以怀疑程序中可能存在内存泄漏,然后使用工具观察程序运行过程中的内存变化,去确定是否真的是内存泄漏导致的!
这里还有一个细节,new失败抛出bad_alloc异常,不一定是内存不足导致的。也可能是程序中有堆内存越界,导致堆内存被破坏引发的。堆内存被破坏,会导致程序导出胡乱的崩溃,一会再new的地方,一会在delete的地方。
对于new失败时抛出bad_alloc异常导致程序崩溃,我们可以让new不要抛出异常,而是像malloc一样在申请内存失败时返回空指针,使用std::nothrow指定new不要抛出异常,如下:
#include <iostream>
int main(){
char *p = NULL;
int i = 0;
do{
p = new(std::nothrow) char[10*1024*1024]; // 每次申请10MB
i++;
Sleep(5);
}
while(p);
if(NULL == p){
std::cout << "分配了 " << (i-1)*10 << " M内存" //分配了 1890 Mn内存第 1891 次内存分配失败
<< "第 " << i << " 次内存分配失败";
}
return 0;
}
此外,还可以使用try...catch来捕获异常:(因为异常被捕获处理了,程序也就不会再崩溃了)
#include <iostream>
using namespace std;
int main(){
char *p;
int i = 0;
try
{
do{
p = new char[10*1024*1024];
i++;
Sleep(5);
}
while(p);
}
catch(const std::exception& e)
{
std::cout << e.what() << "\n"
<< "分配了" << i*10 << "M" << std::endl;
}
return 0;
}
虽然上面两种做法,都能让程序不再崩溃,但程序进程内存不足,业务已经没法正常展开和执行了,让程序还活着,意义也不大了。
4、动态申请内存失败的可能原因分析
动态申请堆内存失败,主要是因为内存不够或堆内存被破坏导致的,下面我们来看几个具体的场景。
4.1、申请的内存过大,进程中没有这么大连续的内存可用了
可能受一些异常数据的影响,申请了很大尺寸的内存。比如前段时间排查一个崩溃问题,当时因为数据有异常,一次性申请了9999*9999*4*2=762MB的堆内存(其中9999为视频图像宽高),进程中没有这么大可用的连续堆内存可用了,所以申请失败了。如果是new操作,则会抛出了一个异常,而程序没有对异常处理,直接导致程序崩溃了。
4.2、程序占用的用户态虚拟内存已经接近上限,申请不到可用的内存了
程序占用了太多的用户态虚拟内存,已经接近程序用户态虚拟内存的上限了。可能是虚拟内存占用太多,也有可能代码中有内存泄露,导致用户态的虚拟内存快被消耗完了。
比如32位程序,系统会给该程序进程分配4GB大小的虚拟地址空间,默认情况下用户态和内核态内存各占一半,即用户态和内核态虚拟内存各占2GB。如果程序占用的虚拟内存占用过大,比如接近2GB的用户态虚拟内存了(比如程序中发生了内存泄漏),再申请大的内存就会申请失败,因为已经接近2GB的上限,没有可用的连续虚拟内存可用了。
4.3、进程中的内存碎片过多
如果进程中在频繁地new和delete,产生了大量的小块内存碎片,可用的内存大多是一小块一小块的小内存块,而我们要申请的是一块长度很长的内存,因为到处是内存碎片,没有这么一大块连续的可用内存,可能就会导致内存申请失败。
4.4、发生堆内存越界,导致堆内存被破坏,导致new操作产生异常
如果程序中发生了堆内存越界,覆盖了部分堆内存区域的头信息或者尾信息:
而系统正是通过这些头信息和尾信息来管理这些堆内存块的。堆内存的头尾区域被覆盖篡改,即堆内存被破坏,则可能会导致系统对堆内存管理上的异常,可能会引发程序莫名其妙的胡乱崩溃,有时崩溃在申请堆内存时,有时崩溃在释放堆内存时!这类问题,我们在项目中多次遇到过!
我们为了搞清楚new申请内存失败的原因,可以在出问题的地方,对该处的new添加一个保护(但不可能对代码中所有new的地方都加这样的保护),我们通过添加try...catch去捕获new抛出的异常,并将异常码打印出来,如下所示:(下面的代码在循环申请内存,直到内存申请失败为止,主要用来测试用)
#include <iostream>
using namespace std;
int main(){
char *p;
int i = 0;
try
{
do{
p = new char[10*1024*1024];
i++;
Sleep(5);
}
while(p);
}
catch(const std::exception& e)
{
std::cout << e.what() << "\n"
<< "分配了" << i*10 << "M" << std::endl;
}
return 0;
}
5、最后
本文结合以往项目中遇到的问题实例,详细总结了引发动态申请内存失败的常见原因和场景,给大家提供一个借鉴或参考。