内存泄漏
定义
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
简介
内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。
随着计算机应用需求的日益增加,应用程序的设计与开发也相应的日趋复杂,开发人员在程序实现的过程中处理的变量也大量增加,如何有效进行内存分配和释放,防止内存泄漏的问题变得越来越突出。例如服务器应用软件,需要长时间的运行,不断的处理由客户端发来的请求,如果没有有效的内存管理,每处理一次请求信息就有一定的内存泄漏。这样不仅影响到服务器的性能,还可能造成整个系统的崩溃。因此,内存管理成为软件设计开发人员在设计中考虑的主要方面
C和C++的内存泄漏
对于C和C++这样的没有Garbage Collection 的语言来讲,我们主要关注两种类型的内存泄漏:
-
堆内存泄漏(Heap leak)。对内存指的是程序执行中依据须要分配通过malloc,realloc new等从堆中分配的一块内存,再是完毕后必须通过调用相应的 free或者delete 删掉。假设程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
-
系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比方 Bitmap,handle ,SOCKET等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
泄漏原因
在C语言中,从变量存在的时间生命周期角度上,把变量分为静态存储变量和动态存储变量两类。静态存储变量是指在程序运行期间分配了固定存储空间的变量而动态存储变量是指在程序运行期间根据实际需要进行动态地分配存储空间的变量。在内存中供用户使用的内存空间分为三部分:
- 程序存储区
- 静态存储区
- 动态存储区
程序中所用的数据分别存放在静态存储区和动态存储区中。
- 静态存储区数据在程序的开始就分配好内存区,在整个程序执行过程中它们所占的存储单元是固定的,在程序结束时就释放,因此静态存储区数据一般为全局变量。
- 动态存储区数据则是在程序执行过程中根据需要动态分配和动态释放的存储单元,动态存储区数据有三类: 函数形参变量、局部变量和函数调用时的现场保护与返回地址。由于动态存储变量可以根据函数调用的需要,动态地分配和释放存储空间,大大提高了内存的使用效率,使得动态存储变量在程序中被广泛使用。
开发人员进行程序开发的过程使用动态存储变量时,不可避免地面对内存管理的问题。程序中动态分配的存储空间,在程序执行完毕后需要进行释放。没有释放动态分配的存储空间而造成内存泄漏,是使用动态存储变量的主要问题。一般情况下,开发人员使用系统提供的内存管理基本函数,如malloc、recalloc、calloc、free等,完成动态存储变量存储空间的分配和释放。但是,当开发程序中使用动态存储变量较多和频繁使用函数调用时,就会经常发生内存管理错误,例如:
- 分配一个内存块并使用其中未经初始化的内容;
- 释放一个内存块,但继续引用其中的内容;
- 子函数中分配的内存空间在主函数出现异常中断时、或主函数对子函数返回的信息使用结束时,没有对分配的内存进行释放;
- 程序实现过程中分配的临时内存在程序结束时,没有释放临时内存。内存错误一般是不可再现的,开发人员不易在程序调试和测试阶段发现,即使花费了很多精力和时间,也无法彻底消除。
产生方式的分类
以产生的方式来分类,内存泄漏可以分为四类:
- 常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏。
- 偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
- 一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。
- 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
检测方法和工具
无论是C还是C++程序,运行时候的变量主要有三种分配方式:
1. 堆分配、
2. 栈分配、
3. 全局和静态内存分配。
内存泄漏主要是发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”,若缺乏语言这样的垃圾回收机制,这样的内存片就无法归还系统。因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。
下面将介绍几种常用的内存检测方法,每种方法均以现有的内存检测工具为分析范例,并对各种方法进行比较。
- 静态分析技术
- 源代码插装技术
- 目标代码插装技术
- 被测代码预处理
详细:https://baike.baidu.com/item/内存泄漏
解决内存泄漏
-
第一:良好的编码习惯,尽量在涉及内存的程序段,检測出内存泄露。当程式稳定之后,在来检測内存泄露时,无疑添加了排除的困难和复杂度。
使用了内存分配的函数,要记得要使用其想用的函数释放掉,一旦使用完成。
Heap memory(堆内存): malloc\realloc ------ free new \new[] ---------- delete \delete[] GlobalAlloc------------GlobalFree
要特别注意数组对象的内存泄漏
MyPointEX *pointArray =new MyPointEX [100]; 其删除形式为: delete []pointArray
-
第二: 重载 new 和 delete。这也是大家编码过程中常用的方法。
//memchecker.h
struct MemIns
{
void * pMem;
int m_nSize;
char m_szFileName[256];
int m_nLine;
MemIns * pNext;
};
class MemManager{
public:
MemManager();
~MemManager();
private:
MemIns *m_pMemInsHead;
int m_nTotal;
public:
static MemManager* GetInstance();
void Append(MemIns *pMemIns);
void Remove(void *ptr);
void Dump();
};
void *operator new(size_t size, const char* szFile, int nLine);
void operator delete(void* ptr, const char* szFile, int nLine);
void operator delete(void* ptr);
void *operator new[](size_t size, const char* szFile, int nLine);
void operator delete[](void* ptr, const char* szFile, int nLine);
void operator delete[](void* ptr);
//memechecker.cpp
#include "Memchecher.h"
#include<stdio.h>
#include<malloc.h>
#include<string.h>
MemManager::MemManager(){
m_pMemInsHead = NULL;
m_nTotal = NULL;
}
MemManager::~MemManager(){}
void MemManager::Append(MemIns *pMemIns){
pMemIns->pNext = m_pMemInsHead;
m_pMemInsHead = pMemIns;
m_nTotal += m_pMemInsHead->m_nSize;
}
void MemManager::Remove(void *ptr){
MemIns * pCur = m_pMemInsHead;
MemIns * pPrev = NULL;
while (pCur){
if (pCur->pMem == ptr){
if (pPrev){
pPrev->pNext = pCur->pNext;
}
else{
m_pMemInsHead = pCur->pNext;
}
m_nTotal -= pCur->m_nSize;
free(pCur);
break;
}
pPrev = pCur;
pCur = pCur->pNext;
}
}
void MemManager::Dump(){
MemIns * pp = m_pMemInsHead;
while (pp){
printf("File is %s\n", pp->m_szFileName);
printf("Size is %d\n", pp->m_nSize);
printf("Line is %d\n", pp->m_nLine);
pp = pp->pNext;
}
}
void PutEntry(void *ptr, int size, const char*szFile, int nLine){
MemIns * p = (MemIns *)(malloc(sizeof(MemIns)));
if (p){
strcpy(p->m_szFileName, szFile);
p->m_nLine = nLine;
p->pMem = ptr;
p->m_nSize = size;
MemManager::GetInstance()->Append(p);
}
}
void RemoveEntry(void *ptr){
MemManager::GetInstance()->Remove(ptr);
}
void *operator new(size_t size, const char*szFile, int nLine){
void * ptr = malloc(size);
PutEntry(ptr, size, szFile, nLine);
return ptr;
}
void operator delete(void *ptr){
RemoveEntry(ptr);
free(ptr);
}
void operator delete(void*ptr, const char * file, int line){
RemoveEntry(ptr);
free(ptr);
}
void* operator new[](size_t size, const char* szFile, int nLine){
void * ptr = malloc(size);
PutEntry(ptr, size, szFile, nLine);
return ptr;
}
void operator delete[](void *ptr){
RemoveEntry(ptr);
free(ptr);
}
void operator delete[](void*ptr, const char*szFile, int nLine){
RemoveEntry(ptr);
free(ptr);
}
#define new new(__FILE__, __LINE__)
MemManager m_memTracer;
MemManager*MemManager::GetInstance(){
return &m_memTracer;
}
void main()
{
int *plen = new int;
*plen = 10;
delete plen;
char *pstr = new char[35];
strcpy(pstr, "hello memory leak");
m_memTracer.Dump();
return;
}
其主要思路是将分配的内存以链表的形式自行管理,使用完成之后从链表中删除,程序结束时可检查该链表,当中记录了内存泄露的文件,所在文件的行数以及泄露的大小。
- 第三,Boost 中的smart pointer
- 第四,一些常见的工具插件。
内存溢出
定义
所谓内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是会产生内存溢出的问题。
常见的溢出主要有:
- 内存分配未成功,却使用了它。
经常使用解决的方法是:
在使用内存之前检查指针是否为NULL。假设指针p 是函数的參数,那么在函数的入口处用assert(p!=NULL)进行检查。假设是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。 - 内存分配尽管成功,可是尚未初始化就引用它。
不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。 - 内存分配成功而且已经初始化,但操作越过了内存的边界。
比如在使用数组时常常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次数非常easy搞错,导致数组操作越界。 - 使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
程序中的对象调用关系过于复杂,实在难以搞清楚某个对象到底是否已经释放了内存,此时应该又一次设计数据结构,从根本上解决对象管理的混乱局面。
来源:https://www.cnblogs.com/mfrbuaa/p/3851742.html