一个方法中有js内存泄漏及解决方法,不知道该怎么改

内存泄漏及简单检测的一种方法 - 致梦想 - 博客园
1.什么是内存泄漏(Memory Leak)?&
&& 简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。
2.内存泄漏的危害性
&&& 从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。主要有以下几种表现形式:
1)cpu资源耗尽:估计是机器没有反应了,键盘,鼠标,以及网络等等。这个在windows上经常看见,特别是中了毒。
2)进程id耗尽:没法创建新的进程了,串口或者telnet都没法创建了。
3)硬盘耗尽: 机器要死了,交换内存没法用,日志也没法用了,死是很正常的。
在我们写程序的时候,一般会使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。如果要避免这个问题,还是要从代码上入手,良好的编码习惯和规范,是避免错误的不二法门。
3.如何检测内存泄漏?
第一:良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。
Heap memory:
malloc\realloc ------ free
new \new[] ---------- delete \delete[]
GlobalAlloc------------GlobalFree
要特别注意数组对象的内存泄漏
MyPointEX *pointArray =new MyPointEX [100];
其删除形式为:
delete []pointArray ;
第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
第三:Boost 中的smart pointer。
第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等。
4.代码示例
&&& 我主要想结合代码讲讲第二个方法,设计思想其实很简单,用到STL中的list。可能有人要问,为什么不用vector呢?list和vector的区别如下:
vector为存储的对象分配一块连续的地址空间,因此对vector中的元素随机访问效率很高。在vecotor中插入或者删除某个元素,需要将现有元素进行复制,移动。如果vector中存储的对象很大,或者构造函数复杂,则在对现有元素进行拷贝时开销较大,因为拷贝对象要调用拷贝构造函数。对于简单的小对象,vector的效率优于list。vector在每次扩张容量的时候,将容量扩展2倍,这样对于小对象来说,效率是很高的。list中的对象是离散存储的,随机访问某个元素需要遍历list。在list中插入元素,尤其是在首尾插入元素,效率很高,只需要改变元素的指针。
vector适用:对象数量变化少,简单对象,随机访问元素频繁
list适用:&&&& 对象数量变化大,对象复杂,插入和删除频繁
1 #include &iostream&
2 #include &stdio.h&
3 #include &stdlib.h&
4 #include &string.h&
5 #include &list&
7 using namespace
9 const int nMaxSize = 26;
10 struct Node
Node *next[nMaxSize];
15 Node *root = NULL;
16 list &Node *& nodeM
18 void TreeCreate()
root = (Node *)malloc(sizeof(Node));
for (int i = 0; i & nMaxS ++i)
root-&next[i] = NULL;
27 void TreeInsert(char *pStr)
int i, j, len = strlen(pStr);
Node *q = NULL;
for (i = 0; i & ++i)
int id = pStr[i] - 'a';
if (p-&next[id] == NULL)
q = (Node *)malloc(sizeof(Node));
if(q != NULL)
nodeMemory.push_back(q);
q-&count = 0;
for (j = 0; j & nMaxS ++j)
q-&next[j] = NULL;
p-&next[id] =
p-&next[id]-&count++;
p = p-&next[id];
52 int TreeQuery(char *pStr)
int i, len = strlen(pStr);
int id = 0;
for (i = 0; i & ++i)
id = pStr[i] - 'a';
p = p-&next[id];
if (p == NULL) return 0;
return p-&
66 void TreeDelete(Node *p)
if (p == NULL)
for (int i = 0; i & nMaxS ++i)
TreeDelete(p-&next[i]);
nodeMemory.remove(p);
79 int main(int argc, char **argv)
char szBuffer[16];
int res = 0;
TreeCreate();
int n = 3;
gets(szBuffer);
if (strlen(szBuffer) == 0)
TreeInsert(szBuffer);
scanf("%s", szBuffer);
res = TreeQuery(szBuffer);
printf("%d\n", res);
for(list&Node *&::iterator it = nodeMemory.begin();it != nodeMemory.end();it++)
cout&&*it&&
cout&&nodeMemory.size()&&
TreeDelete(root);
if(nodeMemory.empty())
cout&&"has delete the tire tree"&&
cout&&nodeMemory.size()&&
system("pause");
代码很简单,list存储的是指向内存空间的指针,每次malloc之后会把这个分配的内存指针push到list中,而当free之后,list的就会删除相应的内容,如果全释放掉了,则list变为空,从而可以判断使用后的内存是否全部释放掉了?
5.最后感想
第一次在博客园发博客,之前为了方便在sina写博客,发现sina不是一般得差,所以搬到这儿来写了,新的开始,希望大家不吝赐教。
博客参考链接:请解释一下“内存泄漏”,这个问题会有什么影响_百度知道
请解释一下“内存泄漏”,这个问题会有什么影响
我有更好的答案
申请了内存不释放就是内存泄露 比如. void GetMemory(int len) { int *p =new int[len]; } p申请了内存,但是函数返回了,没有指针的首地址传出来,不能释放了
简单说明了一下没有工具的情况如何运用VC库中的工具来检查代码的内存泄漏问题。一: 内存泄漏
内存泄漏是编程中常常见到的一个问题,内存泄漏往往会一种奇怪的方式来表现出来,基本上每个程序都表现出不同的方式。 但是一般最后的结果只有两个,一个是程序当掉,一个是系统内存不足。 还有一种就是比较介于中间的结果程序不会当,但是系统的反映时间明显降低,需要定时的Reboot才会正常。
有 一个很简单的办法来检查一个程序是否有内存泄漏。就是是用Windows的任务管理器(Task Manager)。运行程序,然后在任务管理器里面查看 逗内存使用地和地虚拟内存大小地两项,当程序请求了它所需要的内存之后,如果虚拟内存还是持续的增长的话,就说明了这个程序有内存泄漏问题。 当然如果内存泄漏的数目非常的小,用这种方法可能要过很长时间才能看的出来。
当然最简单的办法大概就是用CompuWare的BoundChecker 之类的工具来检测了,不过这些工具的价格对于个人来讲稍微有点奢侈了。
如果是已经发布的程序,检查是否有内存泄漏是又费时又费力。所以内存泄漏应该在Code的生成过程就要时刻进行检查。二: 原因
内存泄漏产生的原因一般是三种情况:分配完内存之后忘了回收;程序Code有问题,造成没有办法回收;某些API函数操作不正确,造成内存泄漏。
1. 内存忘记回收,这个是不应该的事情。但是也是在代码种很常见的问题。分配内存之后,用完之后,就一定要回收。如果不回收,那就造成了内存的泄漏,造成内存泄漏的Code如果被经常调用的话,那内存泄漏的数目就会越来越多的。从而影响整个系统的运行。比如下面的代码:for (int =0;I&100;I++){
Temp = new BYTE[100];}就会产生 100*100Byte的内存泄漏。
2. 在某些时候,因为代码上写的有问题,会导致某些内存想回收都收不回来,比如下面的代码:Temp1 = new BYTE[100];Temp2 = new BYTE[100];Temp2 = Temp1;这样,Temp2的内存地址就丢掉了,而且永远都找不回了,这个时候Temp2的内存空间想回收都没有办法。
3. API函 数应用不当,在Windows提供API函数里面有一些特殊的API,比如FormatMessage。 如果你给它参数中有FORMAT_MESSAGE_ALLOCATE_BUFFER,它会在函数内部New一块内存Buffer出来。但是这个 buffer需要你调用LocalFree来释放。 如果你忘了,那就会产生内存泄漏。三: 检查方法
一 般的内存泄漏检查的确是很困难,但是也不是完全没有办法。如果你用VC的库来写东西的话,那么很幸运的是,你已经有了很多检查内存泄漏的工具,只是你想不 想用的问题了。Visual C++的Debug版本的C运行库(C Runtime Library)。它已经提供好些函数来帮助你诊断你的代码和跟踪内存泄漏。 而且最方便的地方是这些函数在Release版本中完全不起任何作用,这样就不会影响你的Release版本程序的运行效率。
比如下面的例子里面,有一个明细的内存泄漏。当然如果只有这么几行代码的话,是很容易看出有内存泄漏的。但是想在成千上万行代码里面检查内存泄漏问题就不是那么容易了。char * pstr = new char[5];lstrcpy(pstr,&Memory leak&);
如 果我们在Debug版本的Code里面对堆(Heap)进行了操作,包括malloc, free, calloc, realloc, new 和 delete可以利用VC Debug运行时库中堆Debug函数来做堆的完整性和安全性检查。比如上面的代码,lstrcpy的操作明显破坏了pstr的堆结构。使其溢出,并破坏 了临近的数据。那我们可以在调用lstrcpy之后的代码里面加入 _CrtCheckMemory函数。_CrtCheckMemory函数发现前面的lstrcpy使得pstr的堆结构被破坏,会输出这样的报告:emory check error at 0x00372FA5 = 0x79, should be 0xFD.memory check error at 0x00372FA6 = 0x20, should be 0xFD.memory check error at 0x00372FA7 = 0x6C, should be 0xFD.memory check error at 0x00372FA8 = 0x65, should be 0xFD.DAMAGE: after Normal block (#41) at 0x00372FA0.Normal located at 0x00372FA0 is 5 bytes long.
它 告诉说 pstr的长度应该时5个Bytes,但是在5Bytes后面的几个Bytes也被非法改写了。提醒你产生了越界操作。_CrtCheckMemory 的返回值只有TRUE和FALSE,那么你可以用_ASSERTE()来报告出错信息。 上面的语句可以换成 _ASSERTE(_CrtCheckMemory()); 这样Debug版本的程序在运行的时候就会弹出一个警告对话框,这样就不用在运行时候一直盯着Output窗口看了。这个时候按Retry,就可以进入源 代码调试了。看看问题到底出在哪里。
其他类似的函数还有 _CrtDbgReport, _CrtDoForAllClientObjects, _CrtDumpMemoryLeaks,_CrtIsValidHeapPointer, _CrtIsMemoryBlock, _CrtIsValidPointer,_CrtMemCheckpoint, _CrtMemDifference, _CrtMemDumpAllObjectsSince, _CrtMemDumpStatistics, _CrtSetAllocHook, _CrtSetBreakAlloc, _CrtSetDbgFlag,_CrtSetDumpClient, _CrtSetReportFile, _CrtSetReportHook, _CrtSetReportMode
这 些函数全部都可以用来在Debug版本中检查内存的使用情况。具体怎么使用这些函数就不在这里说明了,各位可以去查查MSDN。在这些函数中用处比较大 的,或者说使用率会比较高的函数是_CrtMemCheckpoint, 设置一个内存检查点。这个函数会取得当前内存的运行状态。 _CrtMemDifference 检查两种内存状态的异同。 _CrtMemDumpAllObjectsSince 从程序运行开始,或者从某个内存检查点开始Dump出堆中对象的信息。还有就是_CrtDumpMemoryLeaks当发生内存溢出的时候Dump出堆 中的内存信息。 _CrtDumpMemoryLeaks一般都在有怀疑是内存泄漏的代码后面调用。比如下面的例子:#include &windows.h&#include &crtdbg.h&void main(){char *pstr = new char[5];_CrtDumpMemoryLeaks();}输出:Detected memory leaks! à提醒你,代码有内存泄漏.Dumping objects -&{44} normal block at 0x0 bytes long.Data: & & CD CD CD CD CDObject dump complete.
如 果你双击包含行文件名的输出行,指针将会跳到源文件中内存被分配地方的行。当无法确定那些代码产生了内存泄漏的时候,我们就需要进行内存状态比较。在可疑 的代码段的前后设置内存检查点,比较内存使用是否有可疑的变化。以确定内存是否有泄漏。为此要先定义三个_CrtMemState 对象来保存要比较的内存状态。两个是用来比较,一个用了保存前面两个之间的区别。_CrtMemState Sh1,Sh2,Sh_Dchar *pstr1 = new char[100];_CrtMemCheckPoint(&Sh1); -&设置第一个内存检查点char *pstr2 = new char[100];_CrtMemCheckPoint(&Sh2); -&设置第二个内存检查点_CrtMemDifference(&Sh_Diff, &Sh1, &Sh2); -&检查变化_CrtMemDumpAllObjectsSince(&Sh_Diff); -&Dump变化
如 果你的程序中使用了MFC类库,那么内存泄漏的检查方法就相当的简单了。因为Debug版本的MFC本身就提供一部分的内存泄漏检查。 大部分的new 和delete没有配对使用而产生的内存泄漏,MFC都会产生报告。这个主要是因为MFC重载了Debug版本的new 和delete操作符, 并且对前面提到的API函数重新进行了包装。在MFC类库中检查内存泄漏的Class就叫 CMemoryState,它重新包装了了_CrtMemState,_CrtMemCheckPoint, _CrtMemDifference, _CrtMemDumpAllObjectsSince这些函数。并对于其他的函数提供了Afx开头的函数,供MFC程序使用。比如 AfxCheckMemory, AfxDumpMemoryLeaks 这些函数的基本用法同上面提到的差不多。 CMemoryState和相关的函数的定义都在Afx.h这个头文件中。 有个简单的办法可以跟踪到这些函数的声明。在VC中找到MFC程序代码中下面的代码, 一般都在X.cpp的开头部分#ifdef _DEBUG#define new DEBUG_NEW#undef THIS_FILEstatic char THIS_FILE[] = __FILE__;#endif
把 光标移到DEBUG_NEW上面 按F12,就可以进入Afx.h中定义这些Class和函数的代码部分。 VC中内存泄漏的常规检查办法主要是上面的两种。当然这两种方法只是针对于Debug版本的Heap的检查。如果Release版本中还有内存泄漏,那么 检查起来就麻烦很多了。4 .总结:
实际上Heap的内存泄漏问题是相当的好查的。VC的提供的检查工具也不太少,但是如果是栈出了什么问题,恐怕就麻烦很多了。栈出问题,一般不会产生内存泄漏,但是你的代码的逻辑上很有可能会有影响。这个是最最痛苦的事情。 编程,就是小心,小心再小心而已。
为您推荐:
其他类似问题
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。posts - 437,&
comments - 75,&
trackbacks - 0
.Net内存泄露原因及解决办法
1.&&& 什么是.Net内存泄露
(1).NET 应用程序中的内存
您大概已经知道,.NET 应用程序中要使用多种类型的内存,包括:堆栈、非托管堆和托管堆。这里我们需要简单回顾一下。
以运行库为目标的代码称为托管代码,而不以运行库为目标的代码称为非托管代码。
在运行库的控制下执行的代码称作托管代码。相反,在运行库之外运行的代码称作非托管代码。COM 组件、ActiveX 接口和 Win32 API 函数都是非托管代码的示例。
COM/COM++组件,ActiveX控件,API函数,指针运算,自制的资源文件...这些的非托管的,其它就是托管的.在CLR上编译运行的代码就是托管代码。& 非CLR编译运行的代码就是非托管代码& 。非托管代码用dispose free using 释放&。即使在拥有GC的托管堆上,也有可能发生内存泄漏!
堆栈 堆栈用于存储应用程序执行过程中的局部变量、方法参数、返回值和其他临时值。堆栈按照每个线程进行分配,并作为每个线程完成其工作的一个暂存区。垃圾收集器并不负责清理堆栈,因为为方法调用预留的堆栈会在方法返回时被自动清理。但是请注意,垃圾收集器知道在堆栈上存储的对象的引用。当对象在一种方法中被实例化时,该对象的引用(32 位或 64 位整型值,取决于平台类型)将保留在堆栈中,而对象自身却存储于托管堆中,并在变量超出范围时被垃圾收集器收集。
非托管堆 非托管堆用于运行时数据结构、方法表、Microsoft 中间语言 (MSIL)、JITed 代码等。非托管代码根据对象的实例化方式将其分配在非托管堆或堆栈上。托管代码可通过调用非托管的 Win32& API 或实例化 COM 对象来直接分配非托管堆内存。CLR 出于自身的数据结构和代码原因广泛地使用非托管堆。
托管堆 托管堆是用于分配托管对象的区域,同时也是垃圾收集器的域。CLR 使用分代压缩垃圾收集器。垃圾收集器之所以称为分代式,是由于它将垃圾收集后保留下来的对象按生存时间进行划分,这样做有助于提高性能。所有版本的 .NET Framework 都采用三代分代方法:第 0 代、第 1 代和第 2 代(从年轻代到年老代)。垃圾收集器之所以称为压缩式,是因为它将对象重新定位于托管堆上,从而能够消除漏洞并保持可用内存的连续性。移动大型对象的开销很高,因此垃圾收集器将这些大型对象分配在独立的且不会压缩的大型对象堆上。有关托管堆和垃圾收集器的详细信息,请参阅 Jeffrey Richter 所著的分为两部分的系列文章&垃圾收集器:Microsoft .NET Framework 中的自动内存管理&和&垃圾收集器 - 第 2 部分:Microsoft .NET Framework 中的自动内存管理&。虽然该文的写作是基于 .NET Framework 1.0,而且 .NET 垃圾收集器已经有所改进,但是其中的核心思想与 1.1 版或 2.0 版是保持一致的。
可能很多.NET的用户(甚至包括一些dot Net开发者)对Net的内存泄露不是很了解,甚至会说.Net不存在内存泄露,因为&不是有GC机制吗?----&恩,是有这么回事,它可以让你在通常应用中不用考虑令人头疼的资源释放问题,但很遗憾的是这个机制不保证你开发的程序就不存在内存泄露。甚至可以说,dot Net中内存泄露是很常见的。这是因为: 一方面,GC机制本身的缺陷造成的;另一方面,Net中托管资源和非托管资源的处理是有差异的,托管资源的处理是由GC自动执行的(执行时机是不可预知的),而非托管资源 (占少部分,比如文件操作,网络连接等)必须显式地释放,否则就可能造成泄露。综合起来说的话,由于托管资源在Net中占大多数,通常不做显式的资源释放是可以的,不会造成明显的资源泄露,而非托管资源则不然,是发生问题的主战场,是最需要注意的地方。 另外,很多情况下,衰老测试主要关注的是有没有内存泄露的发生,而对其他泄露的重视次之。这是因为,内存跟其他资源是正相关的,也就是说没有内存泄露的发生,其他泄露的发生概率也较小,其根本原因在于几乎所有的资源最后都会在内存上有所反应。
一提到托管代码中出现内存泄漏,很多开发人员的第一反应都认为这是不可能的。毕竟垃圾收集器 (GC) 会负责管理所有的内存,没错吧?但要知道,垃圾收集器只处理托管内存。基于 Microsoft& .NET Framework 的应用程序中大量使用了非托管内存,这些非托管内存既可以被公共语言运行库 (CLR) 使用,也可以在与非托管代码进行互操作时被程序员显式使用。在某些情况下,垃圾管理器似乎在逃避自己的职责,没有对托管内存进行有效处理。这通常是由于不易察觉的(也可能是非常明显的)编程错误妨碍了垃圾收集器的正常工作而造成的。作为经常与内存打交道的程序员,我们仍需要检查自己的应用程序,确保它们不会发生内存泄漏并能够合理有效地使用所需内存。
2 内存泄漏的种类及原因
(1)堆栈内存泄漏
虽然有可能出现堆栈空间不足而导致在受托管的情况下引发 StackOverflowException 异常,但是方法调用期间使用的任何堆栈空间都会在该方法返回后被回收。因此,实际上只有在两种情况下才会发生堆栈空间泄漏。一种情况是进行一种极其耗费堆栈资源并且从不返回的方法调用,从而使关联的堆栈帧无法得到释放。另一种情况是发生线程泄漏,从而使线程的整个堆栈发生泄漏。如果应用程序为了执行后台工作而创建了工作线程,但却忽略了正常终止这些进程,则可引起线程泄漏。默认情况下,最新桌面机和服务器版的 Windows& 堆栈大小均为 1MB。因此如果应用程序的 Process/Private Bytes 定期增大 1MB,同时 .NET CLR LocksAndThreads/# of current logical Threads 也相应增大,那么罪魁祸首很可能是线程堆栈泄漏。下图&显示了(恶意的)多线程逻辑导致的不正确的线程清理示例。
&Figure&&清理错误线程
using System.T
namespace MsdnMag.ThreadForker {
& class Program {
&&& static void Main() {
&&&&& while(true) {
&&&&&&& Console.WriteLine(
&&&&&&&&& "Press &ENTER& to fork another thread...");
&&&&&&& Console.ReadLine();
&&&&&&& Thread t = new Thread(new ThreadStart(ThreadProc));
&&&&&&& t.Start();
&&& static void ThreadProc() {
&&&&& Console.WriteLine("Thread #{0} started...",
&&&&&&&&Thread.CurrentThread.ManagedThreadId);
&&&&& // Block until current thread terminates - i.e. wait forever
&&&&& Thread.CurrentThread.Join();
当一个线程启动后会显示其线程 ID,然后尝试自联接。联接会导致调用线程停止等待另一线程的终止。这样该线程就会陷入一个类似于先有鸡还是先有蛋的尴尬局面之中 & 线程要等待自身的终止。在任务管理器下查看该程序,会发现每次按 &Enter& 时,其内存使用率会增长 1MB(即线程堆栈的大小)。
每次经过循环时,Thread 对象的引用都会被删除,但垃圾收集器并未回收分配给线程堆栈的内存。托管线程的生存期并不依赖于创建它的 Thread 对象。如果您只是因为丢失了所有与 Thread 对象相关联的引用而不希望垃圾收集器将一个仍在运行的进程终止,这种不依赖性是非常有好处的。由此可见,垃圾收集器只是收集 Thread 对象,而非实际托管的线程。只有在其 ThreadProc 返回后或者自身被直接终止的情况下,托管线程才会退出(其线程堆栈的内存不会释放)。因此,如果托管线程的终止方式不正确,分配至其线程堆栈的内存就会发生泄漏。
(2)非托管堆内存泄漏
如果总的内存使用率增加,而逻辑线程计数和托管堆内存并未增加,则表明非托管堆出现内存泄漏。我们将对导致非托管堆中出现内存泄漏的一些常见原因进行分析,其中包括与非托管代码进行互操作、终结器被终止以及程序集泄漏。
与非托管代码进行互操作:这是内存泄漏的起因之一,涉及到与非托管代码的互操作,例如在 COM Interop 中通过 P/Invoke 和 COM 对象使用 C 样式的 DLL。垃圾收集器无法识别非托管内存,而正是在托管代码的编写过程中错误地使用了非托管内存,才导致内存出现泄漏。如果应用程序与非托管代码进行互操作,要逐步查看代码并检查非托管调用前后内存的使用情况,以验证内存是否被正确回收。如果内存未被正确回收,则使用传统的调试方法在非托管组件中查找泄漏。
终结器被终止:当一个对象的终结器未被调用,并且其中含有用于清理对象所分配的非托管内存的代码时,会造成隐性泄漏。在正常情况下,终结器都将被调用,但是 CLR 不会对此提供任何保证。虽然未来可能会有所变化,但是目前的 CLR 版本仅使用一个终结器线程。请考虑这样一种情况,运行不正常的终结器试图将信息记录到脱机的数据库。如果该运行不正常的终结器反复尝试对数据库进行错误的访问而从不返回,则&运行正常&的终结器将永远没有机会运行。该问题会不时出现,因为这取决于终结器在终结队列中的位置以及其他终结器采取何种行为。
当 AppDomain 拆开时,CLR 将通过运行所有终结器来尝试清理终结器队列。被延迟的终结器可阻止 CLR 完成 AppDomain 拆开。为此,CLR 在该进程上做了超时操作,随后将停止该终止进程。但是这并不意味着世界末日已经来临。因为通常情况下,大多数应用程序只有一个 AppDomain,而只有进程被关闭才会导致 AppDomain 的拆开。当操作系统进程被关闭,操作系统会对该进程资源进行恢复。但不幸的是,在诸如 ASP.NET 或 SQL Server& 之类的宿主情况下,AppDomain 的拆开并不意味着宿主进程的结束。另一个 AppDomain 会在同一进程中启动。任何因自身终结器未运行而被组件泄漏的非托管内存都将继续保持未引用状态,无法被访问,并且占用一定空间。因为内存的泄漏会随着时间的推移越来越严重,所以这将带来灾难性的后果。
在 .NET 1.x中,唯一的解决方法是结束并重新启动该进程。.NET Framework 2.0 中引入了关键的终结器,指明在 AppDomain 关闭期间,终结器将清理非托管资源并必须获得运行的机会。有关详细信息,请参阅 Stephen Toub 的文章:&利用 .NET Framework 的可靠性功能确保代码稳定运行&。
程序集泄漏:程序集泄漏相对来说要常见一些。一旦程序集被加载,它只有在 AppDomain 被卸载的情况下才能被卸载。程序集泄漏也正是由此引发的。大多数情况下,除非程序集是被动态生成并加载的,否则这根本不算个问题。下面我们就来看一看动态代码生成造成的泄漏,特别要详细分析 XmlSerializer 的泄漏。
动态代码生成有时会泄漏我们需要动态生成代码。也许应用程序具有与 Microsoft Office 相似的宏脚本编写接口来提高其扩展性。也许某个债券定价引擎需要动态加载定价规则,以便最终用户能够创建自己的债券类型。也许应用程序是用于 Python 的动态语言运行库/编译器。在很多情况下,出于性能方面的考虑,最好是通过编写宏、定价规则或 MSLI 代码来解决问题。您可以使用 System.CodeDom 来动态生成 MSLI。
下图&中的代码可在内存中动态生成一个程序集。该程序集可被重复调用而不会出现问题。遗憾的是,一旦宏、定价规则或代码有所改变,就必须重新生成新的动态程序集。原有的程序集将不再使用,但是却无法从内存中清除,加载有程序集的 AppDomain 也无法被卸载。其代码、JITed 方法和其他运行时数据结构所用的非托管堆内存已经被泄漏。(托管内存也在动态生成的类上以任意静态字段的形式被泄漏。)要检测到这一问题,我们尚无良方妙计。如果您正使用 System.CodeDom 动态地生成 MSLI,请检查是否重新生成了代码。如果有代码生成,那么您的非托管堆内存正在发生泄漏。
CodeCompileUnit program = new CodeCompileUnit();
CodeNamespace ns = new
&&CodeNamespace("MsdnMag.MemoryLeaks.CodeGen.CodeDomGenerated");
ns.Imports.Add(new CodeNamespaceImport("System"));
program.Namespaces.Add(ns);
CodeTypeDeclaration class1 = new CodeTypeDeclaration("CodeDomHello");
ns.Types.Add(class1);
CodeEntryPointMethod start = new CodeEntryPointMethod();
start.ReturnType = new CodeTypeReference(typeof(void));
CodeMethodInvokeExpression cs1 = new CodeMethodInvokeExpression(
& new CodeTypeReferenceExpression("System.Console"), "WriteLine",
&&&&new CodePrimitiveExpression("Hello, World!"));
start.Statements.Add(cs1);
class1.Members.Add(start);
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults results = provider.CompileAssemblyFromDom(
& new CompilerParameters(), program);
目前有两种主要方法可解决这一问题。第一种方法是将动态生成的 MSLI 加载到子 AppDomain 中。子 AppDomain 能够在所生成的代码发生改变时被卸载,并运行一个新的子 AppDomain 来托管更新后的 MSLI。这种方法在所有版本的 .NET Framework 中都是行之有效的。
.NET Framework 2.0 中还引入了另外一种叫做轻量级代码生成的方法,也称动态方法。使用 DynamicMethod 可以显式发出 MSLI 的操作码来定义方法体,然后可以直接通过 DynamicMethod.Invoke 或通过合适的委托来调用 DynamicMethod。
DynamicMethod dm = new DynamicMethod("tempMethod" +
&&Guid.NewGuid().ToString(), null, null, this.GetType());
ILGenerator il = dm.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello, World!");
MethodInfo cw = typeof(Console).GetMethod("WriteLine",
&&new Type[] { typeof(string) });
il.Emit(OpCodes.Call, cw);
dm.Invoke(null, null);
动态方法的主要优势是 MSLI 和所有相关代码生成数据结构均被分配在托管堆上。这意味着一旦 DynamicMethod 的最后一个引用超出范围,垃圾收集器就能够回收内存。
XmlSerializer 泄漏:.NET Framework 中的某些部分(例如 XmlSerializer)会在内部使用动态代码生成。请看下列典型的 XmlSerializer 代码:
XmlSerializer serializer = new XmlSerializer(typeof(Person));
serializer.Serialize(outputStream, person);
XmlSerializer 构造函数将使用反射来分析 Person 类,并藉此生成一对由 XmlSerializationReader 和 XmlSerializationWriter 派生而来的类。它将创建临时的 C# 文件,将结果文件编译成临时程序集,并最终将该程序集加载到进程。通过这种方式生成的代码同样需要相当大的开销。因此 XmlSerializer 对每种类型的临时程序集进行缓存。也就是说,下一次为 Person 类创建 XmlSerializer 时,会使用缓存的程序集,而不再生成新的程序集。
默认情况下,XmlSerializer 所使用的 XmlElement 名称就是该类的名称。因此,Person 将被序列化为:
&?xml version="1.0" encoding="utf-8"?&
&Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
&&xmlns:xsd="http://www.w3.org/2001/XMLSchema"&
&Id&5d49c002-089d-4445-ac4a-acb&/Id&
&FirstName&John&/FirstName&
&LastName&Doe&/LastName&
有时有必要在不改变类名称的前提下改变根元素的名称。(要与现有架构兼容可能需要根元素名称。)因此 Person 可能需要被序列化为 &PersonInstance&。XmlSerializer 构造函数能够很方便地被重载,将根元素名称作为第二参数,如下所示:
XmlSerializer serializer = new XmlSerializer(typeof(Person),
&&new XmlRootAttribute("PersonInstance"));
当应用程序开始对 Person 对象进行序列化/反序列化时,一切运转正常,直至引发 OutOfMemoryException。对 XmlSerializer 构造函数的重载并不会对动态生成的程序集进行缓存,而是在每次实例化新的 XmlSerializer 时生成新的临时程序集。这时应用程序以临时程序集的形式泄漏非托管内存。
要修复该泄漏,请在类中使用 XmlRootAttribute 以更改序列化类型的根元素名称:
[XmlRoot("PersonInstance")]
public class Person {
如果直接将属性赋予类型,则 XmlSerializer 对为类型所生成的程序集进行缓存,从而避免了内存的泄漏。如果需要对根元素名称进行动态切换,应用程序能够利用工厂对其进行检索,从而对 XmlSerializer 实例自身进行缓存。
XmlSerializer serializer = XmlSerializerFactory.Create(
& typeof(Person), "PersonInstance");
XmlSerializerFactory 是我创建的一个类,它可以使用 PersonInstance 根元素名称来检查 Dictionary&Tkey, Tvalue& 中是否包含有用于 Person 的 Xmlserializer。如果包含,则返回该实例。如果不包含,则创建一个新的实例,并将其存储在哈希表中返回给调用方。
(3)&泄漏&托管堆内存
现在让我们关注一下托管内存的&泄漏&。在处理托管内存时,垃圾收集器会帮助我们完成绝大部分的工作。我们需要向垃圾收集器提供工作所需的信息。但是,在很多场合下,垃圾收集器无法有效地工作,导致需要使用比正常工作要求更高的托管内存。这些情况包括大型对象堆碎片、不必要的根引用以及中年危机。
(4)大型对象堆碎片 如果一个对象的大小为 85,000 字节或者更大,就要被分配在大型对象堆上。请注意,这里是指对象自身的大小,并非任何子对象的大小。以下列类为例:
public class Foo {
& private byte[] m_buffer = new byte[90000]; // large object heap
由于 Foo 实例仅含有一个 4 字节(32 位框架)或 8 字节(64 位框架)的缓冲区引用,以及一些 .NET Framework 使用的内务数据,因此将被分配在普通的分代式托管堆上。缓冲区将分配在大型对象堆上。
与其他的托管堆不同,由于移动大型对象耗费资源,所以大型对象堆不会被压缩。因此,当大型对象被分配、释放并清理后,就会出现空隙。根据使用模式的不同,大型对象堆中的这些空隙可能会使内存使用率明显高于当前分配的大型对象所需的内存使用率。本月下载中包含的 LOHFragmentation 应用程序会在大型对象堆中随机分配和释放字节数组,从而用实例证实了这一点。应用程序运行几次后,能通过释放字节数组的方式创建出恰好与空隙相符的新的字节数组。在应用程序的另外几次运行中,则未出现这种情况,内存需要量远远大于当前分配的字节数组的内存需要量。您可以使用诸如 CLRProfiler 的内存分析器来将大型对象堆的碎片可视化。下图&中的红色区域为已分配的字节数组,而白色区域则代表未分配的空间。
图&CLRProfiler 中的大型对象堆&(单击该图像获得较大视图)
目前尚无一种单一的解决方案能够避免大型对象堆碎片的产生。您可以使用类似 CLRProfiler 的工具对应用程序的内存使用情况,特别是大型对象堆中的对象类型进行检查。如果碎片是由于重新分配缓冲区而产生的,则请保持固定数量的重用缓冲区。如果碎片是由于大量字符串串连而产生的,请检查 System.Text.StringBuilder 类是否能够减少创建临时字符串的数量。基本策略是要确定如何降低应用程序对临时大型对象的依赖,而临时大型对象正是大型对象堆中产生空隙的原因所在。
(5)不必要的根引用 让我们思考一下垃圾收集器是如何决定回收内存的时间。当 CLR 试图分配内存并保留不足的内存时,它就在扮演着垃圾收集器的角色。垃圾收集器列出了所有的根引用,包括位于任何线程的调用堆栈上的静态字段和域内局部变量。垃圾收集器将这些引用标记为可访问,并跟据这些对象所包含的引用,将其同样标记为可访问。这一过程将持续进行,直至所有可访问的引用均被访问。任何没有被标记的对象都是无法访问的,因此是垃圾。垃圾收集器对托管堆进行压缩,整理引用以指向它们在堆中的新位置,并将控件返回给 CLR。如果释放充足的内存,则使用此释放的内存进行分配。如果释放的内存不足,则向操作系统请求额外的内存。
如果我们忘记清空根引用,系统会立即阻止垃圾收集器有效地释放内存,从而导致应用程序需要更多的内存。问题可能微妙,例如一种方法,它能够在做出与查询数据库或调用某个 Web 服务相类似的远程调用前为临时对象创建大型图形。如果垃圾收集发生在远程调用期间,则整个图形被标记为可访问的,并不会收集。这样会导致更大的开销,因为在收集中得以保留的对象将被提升到下一代,这将引起所谓的中年危机。
(6)中年危机 中年危机不会使应用程序去购买一辆保时捷。但它却可以造成托管堆内存的过度使用,并使垃圾收集器花费过多的处理器时间。正如前面所提到的,垃圾收集器使用分代式算法,采取试探性的推断,它会认为如果对象已经存活一段时期,则有可能存活更长的一段时期。例如,在 Windows 窗体应用程序中,应用程序启动时会创建主窗体,主窗体关闭时应用程序则退出。对于垃圾收集器来说,持续地验证主窗体是否正在被引用是一件浪费资源的事。当系统需要内存以满足分配请求时,会首先执行第 0 代收集。如果没有足够的可用内存,则执行第 1 代收集。如果仍然无法满足分配请求,则继续执行第 2 代收集,这将导致整个托管堆以极大的开销进行清理工作。第 0 代收集的开销相对较低,因为只有当前被分配的对象才被认为是需要收集的。
如果对象有继续存活至第 1 代(或更严重至第 2 代)的趋势,但却随即死亡,此时就会出现中年危机。这样做的效果是使得开销低的第 0 代收集转变为开销大得多的第 1 代(或第 2 代)收集。为什么会发生这种现象呢?请看下面的代码:
class Foo {
& ~Foo() { }
对象将始终在第 1 代收集中被回收!终结器 ~Foo() 使我们可以实现对象的代码清理,除非强行终止 AppDomain,否则代码将在对象内存被释放前运行。垃圾收集器的任务是尽快地释放尽可能多的托管内存。终结器是由用户编写的代码,并且毫无疑问可以执行任何操作。虽然我们并不建议,但是终结器也会执行一些愚蠢的操作,例如将日志记录到数据库或调用 Thread.Sleep(int.MaxValue)。因此,当垃圾收集器发现具有终结器但未被引用的对象时,会将该对象加入到终结队列中,并继续工作。该对象由此在垃圾收集中得以保留,被提升一代。这里甚至为其准备了一个性能计数器:.NET CLR Memory-Finalization Survivors,可显示最后一次垃圾收集期间由于具有终结器而得以保留的对象的数量。最后,终结器线程将运行对象的终结器,随后对象即被收集。但此时您已经从开销低的第 0 代收集转变为第 1 代收集,而您仅仅是添加了一个终结器!
大多数情况下,编写托管代码时终结器并不是必不可少的。只有当托管对象具有需要清理的非托管资源的引用时,才需要终结器。而且即使这样,您也应该使用 SafeHandle 派生类型来对非托管资源进行包装,而不要使用终结器。此外,如果您使用非托管资源或其他实现 Idispoable 的托管类型,请实现 Dispose 模式来让使用对象的用户大胆地清理资源,并避免使用任何相关的终结器。
如果一个对象仅拥有其他托管对象的引用,垃圾收集器将对未引用的对象进行清理。这一点与 C++ 截然不同,在 C++ 中必须在子对象上调用删除命令。如果终结器为空或仅仅将子对象引用清空,请将其删除。将对象不必要地提升至更高一代将对性能造成影响,使清理开销更高。
还有一些做法会导致中年危机,例如在进行查询数据库、在另一线程上阻塞或调用 Web 服务等阻塞调用之前保持对对象的持有。在调用过程中,可以发生一次或多次收集,并由此使得开销低的第 0 代对象提升至更高一代,从而再次导致更高的内存使用率和收集成本。
还有一种情况,它与事件处理程序和回调一起发生并且更难理解。我将以 ASP.NET 为例,但同样类型的问题也会发生在任何应用程序中。考虑一下执行一次开销很大的查询,然后等上 5 分钟才可以缓存查询结果的情况。查询是属于页面查询,并基于查询字符串参数来进行。当一项内容从缓存中删除时,事件处理程序将进行记录,以监视缓存行为。(参见下图)。
&记录从缓存中移除的项
protected void Page_Load(object sender, EventArgs e) {
& string cacheKey = buildCacheKey(Request.Url, Request.QueryString);
& object cachedObject = Cache.Get(cacheKey);
& if(cachedObject == null) {
&&& cachedObject = someExpensiveQuery();
&&& Cache.Add(cacheKey, cachedObject, null,
&&&&&&Cache.NoAbsoluteExpiration,
&&&&& TimeSpan.FromMinutes(5), CacheItemPriority.Default,
&&&&&&new CacheItemRemovedCallback(OnCacheItemRemoved));
& ... // Continue with normal page processing
private void OnCacheItemRemoved(string key, object value,
&&&&&&&&&&&&&&& CacheItemRemovedReason reason) {
& ... // Do some logging here
看上去正常的代码实际上隐含着严重的错误。所有这些 ASP.NET Page 实例都变成了&永世长存&的对象。OnCacheItemRemoved 是一个实例方法,CacheItemRemovedCallback 委托中包含了一个隐式的&this&指针,这里的&this&即为 Page 实例。该委托被添加至 Cache 对象。这样,就会产生一个从 Cache 到委托再到 Page 实例的依赖关系。在进行垃圾收集时,可以一直从根引用(Cache 对象)访问 Page 实例。这时,Page 实例(以及在呈现时它所创建的所有临时对象)至少需要等待五分钟才能被收集,在此期间,它们都有可能被提升至第 2 代。幸运地是,有一种简单的方法能够解决该示例中的问题。请将回调函数变为静态。Page 实例上的依赖关系就会被打破,从而可以像第 0 代对象一样以很低的开销来进行收集。
3..Net内存泄露的检测
(1)如何检测泄漏
很多迹象能够表明应用程序正在发生内存泄漏。或许应用程序正在引发 OutOfMemoryException。或许应用程序因启动了虚拟内存与硬盘的交换而变得响应迟缓。或许出现任务管理器中内存的使用率逐渐(也可能突然地)上升。当怀疑应用程序发生内存泄漏时,必须首先确定是哪种类型的内存发生泄漏,以便您将调试工作的重点放在合适的区域。使用 PerfMon 来检查用于应用程序的下列性能计数器:Process/Private Bytes、.NET CLR Memory/# Bytes in All Heaps 和 .NET CLR LocksAndThreads/# of current logical Threads。Process/Private Bytes 计数器用于报告系统中专门为某一进程分配而无法与其他进程共享的所有内存。.NET CLR Memory/# Bytes in All Heaps 计数器报告第 0 代、第 1 代、第 2 代和大型对象堆的合计大小。.NET CLR LocksAndThreads/# of current logical Threads 计数器报告 AppDomain 中逻辑线程的数量。如果应用程序的逻辑线程计数出现意想不到的增大,则表明线程堆栈发生泄漏。如果 Private Bytes 增大,而 # Bytes in All Heaps 保持不变,则表明非托管内存发生泄漏。如果上述两个计数器均有所增加,则表明托管堆中的内存消耗在增长。  有没有内存泄露的发生?判断依据是那些?
  如果程序报&Out of memory&之类的错误,事实上也占据了很大部分的内存,应该说是典型的内存泄露,这种情况属于彻底的Bug,解决之道就是找到问题点,改正。但我的经验中,这种三下两下的就明显的泄露的情况较少,除非有人在很困的情况下编码,否则大多是隐性或渐进式地泄露,这种需经过较长时间的衰老测试才能发现,或者在特定条件下才出现,对这种情况要确定问题比较费劲,有一些工具(详见1.3)可以利用,但我总感觉效果一般,也可能是我不会使用吧,我想大型程序估计得无可奈何的用这个,详细的参见相关手册。
  需要强调的是,判断一个程序是不是出现了"memory leak",关键不是看它占用的内存有多大,而是放在一个足够长的时期(程序进入稳定运行状态后)内,看内存是不是还是一直往上涨,因此,刚开始的涨动或者前期的涨动不能做为泄露的充分证据。
  以上是些比较感性的说法,实际操作中是通过一些性能计数器来测定的。大多数时候,主要关注Process 里的以下几个指标就能得出结论,如果这些量整体来看是持续上升的,基本可以判断是有泄露情况存在的。
  A.Handle Count
  B.Thread Count
  C.Private Bytes
  D.Virtual Bytes
  E.Working Set
  F.另外.NET CLR Memory下的Bytes in all heeps也是我比较关注的。
  通过观察,如果发现这些参数是在一个区间内震荡的,应该是没有大的问题,但如果是一个持续上涨的状态,那就得注意,很可能存在内存泄露。
(2)内存泄露诊断工具
  1.1如何测定以上的性能计数器
  大多使用windows自带的perfmon.msc。
  1.2其他一些重要的性能计数器
  重要的计数器
  1.3其他检测工具
  用过的工具里面CLRProfiler 和dotTrace还行,windeg也还行。不过坦白的说,准确定位比较费劲,最好还是按常规的该Dispose的加Dispose,也可以加 GC.Collect()。
4.如何防止内存泄露
(1) Dispose()的使用
  如果使用的对象提供Dispose()方法,那么当你使用完毕或在必要的地方(比如Exception)调用该方法,特别是对非托管对象,一定要加以调 用,以达到防止泄露的目的。另外很多时候程序提供对Dispose()的扩展,比如Form,在这个扩展的Dispose方法中你可以把大对象的引用什么 的在退出前释放。
  对于DB连接,COM组件(比如OLE组件)等必须调用其提供的Dispose方法,没有的话最好自己写一个。
(2) using的使用
using除了引用Dll的功用外,还可以限制对象的适用范围,当超出这个界限后对象自动释放,比如
using语句的用途
定义一个范围,将在此范围之外释放一个或多个对象。
可以在 using 语句中声明对象:
using (Font font1 = new Font("Arial", 10.0f))
&&&// use font1
或者在 using 语句之前声明对象:
Font font2 =
new Font("Arial", 10.0f);
using (font2)
// use font2
可以有多个对象与 using 语句一起使用,但是必须在 using 语句内部声明这些对象:
using (Font font3 = new Font("Arial", 10.0f),font4 = new Font("Arial", 10.0f))
// Use font3
and font4.
(3) 事件的卸载
  这个不是必须的,推荐这样做。之前注册了的事件,关闭画面时应该手动注销,有利于GC回收资源。
(4) API的调用
  一般的使用API了就意味着使用了非托管资源,需要根据情况手动释放所占资源,特别是在处理大对象时。 4.5继承 IDisposable实现自己内存释放接口 Net 如何继承IDisposable接口,实现自己的Dispose()函数
(5)弱引用(WeakReference )
  通常情况下,一个实例如果被其他实例引用了,那么他就不会被GC回收,而弱引用的意思是,如果一个实例没有被其他实例引用(真实引用),而仅仅是被弱引 用,那么他就会被GC回收。
(6)析构函数(Finalize())
  使用了非托管资源的时候,可以自定义析构函数使得对象结束时释放所占资源;
  对仅使用托管资源的对象,应尽可能使用它自身的Dispose方法,一般不推荐自定义析构函数。
根据普遍意义上的内存泄漏定义,大多数的.NET内存对象在不再被使用后都会有短暂的一段时间的内存泄漏,因为要等待下一个GC时才有可能会被释放。但这种情况并不会对系统造成大的危害。
其实真正影响系统的严重内存泄漏情况如:
1:大对象的分配。
根据CLR的设计,.NET中的大对象将分配在托管堆内的一个特殊的区域,在回收大对象的时候,并不会像变通区域回收完成时要做内存碎片整理,这是因为这个区域都是大对象,对大对象的移动成本太大了。因此如果本来有三个连续的大对象,现在中间这个要释放掉了,然后新分配进来一个稍小点的大对象,这样势必在中间产生小的内存碎片,这个部分又无法利用。就造成了内存泄漏,并且除非碎片相邻的大对象被释放掉外,没法解决。&&
因此在编程时要注意大对象的操作,尽量减少大对象的分配次数。
2:避免根引用对象的分配
所谓的根引用对象就是那些GC不会去释放的对象引用。比如类的公共静态变量。
GC会视该变量对象在整个程序生命周期中都有效。因此就不会释放它。当它本身比较大,或者它内部又想用了其它很多对象时,这一连串的对象都无法在整个生命周期中得到释放。造成了较大的内存泄漏,应该时时注意这种风险的发生。
3:不合理的Finalize()
方法定义。
以上已经就 .NET 应用程序中能够导致内存泄漏或内存消耗过度的各种问题进行了讨论。虽然 .NET 可减少您对内存方面的关注程度,但是您仍必须关注应用程序的内存使用情况,以确保应用程序高效正常运行。虽然应用程序被托管,但这并不意味着您可以依靠垃圾收集器就能解决所有问题而将良好的软件工程实践束之高阁。虽然在应用程序的开发和测试阶段,您必须对其内存性能进行持续不断的监视。但是这样做非常值得。要记住,只有让用户满意才称得上是功能良好的应用程序。
关于.NET有一个鲜有人言及的问题,它和使用动态代码生成有关。简而言之,在XML序列化、正则表达式和XLST转换中用到的动态代码生成功能会引起内存泄漏。
尽管公共语言运行时(Common Language Runtime,CLR)能卸载整个应用程序域(App
Domain),但是它无法卸载个别的Assemblies。代码生成依赖于创建临时Assemblies。通常这些Assemblies会被加载进主应用程序域中,这也就是说,不到应用程序退出时,它们都无法被卸载。
对于诸如XML序列化的库来说,这个问题并不大。通常,一个给定类型的序列化代码都会缓存起来,这样应用程序则被限制在每类型只有一个临时Assembly。但有些XMLSerializer的重载没有使用缓存。假如开发人员使用了它们,又没有提供在一定程度的应用程序级别的缓存,那么随着本质上相同的代码的新实例不断被加载到内存中,内存将会慢慢发生泄漏。
阅读(...) 评论()}

我要回帖

更多关于 c 内存泄漏及解决方法 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信