Welcome

首页 / 软件开发 / .NET编程技术 / 调试内存泄漏的应用程序: 发现并防止托管代码中出现内存泄漏

调试内存泄漏的应用程序: 发现并防止托管代码中出现内存泄漏2011-02-26 MSDN James Kovacs本文讨论:

理解托管应用程序中的内存泄漏问题

.NET 应用程序中所用的非托管内存

帮助 .NET 垃圾收集器发挥应有功效

本文使用了以下技术:

.NET Framework

目录

.NET 应用程序中的内存

检测泄漏

堆栈内存泄漏

非托管堆内存泄漏

“泄漏”托管堆内存

总结

一提到托管代码中出现内存泄漏,很多开发人员的第一反应都认为这是不可能的。毕竟垃圾收集器 (GC) 会负责管理所有的内存,没错吧?但要知道,垃圾收集器只处理托管内存。基于 Microsoft® .NET Framework 的应用程序中大量使用了非托管内存,这些非托管内存既可以被公共语言运行库 (CLR) 使用,也可以在与非托管代码进行互操作时被程序员显式使用。在某些情况下,垃圾管理器似乎在逃避自己的职责,没有对托管内存进行有效处理。这通常是由于不易察觉的(也可能是非常明显的)编程错误妨碍了垃圾收集器的正常工作而造成的。作为经常与内存打交道的程序员,我们仍需要检查自己的应用程序,确保它们不会发生内存泄漏并能够合理有效地使用所需内存。

.NET 应用程序中的内存

您大概已经知道,.NET 应用程序中要使用多种类型的内存,包括:堆栈、非托管堆和托管堆。这里我们需要简单回顾一下。

堆栈 堆栈用于存储应用程序执行过程中的局部变量、方法参数、返回值和其他临时值。堆栈按照每个线程进行分配,并作为每个线程完成其工作的一个暂存区。垃圾收集器并不负责清理堆栈,因为为方法调用预留的堆栈会在方法返回时被自动清理。但是请注意,垃圾收集器知道在堆栈上存储的对象的引用。当对象在一种方法中被实例化时,该对象的引用(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 版是保持一致的。

检测泄漏

很多迹象能够表明应用程序正在发生内存泄漏。或许应用程序正在引发 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 保持不变,则表明非托管内存发生泄漏。如果上述两个计数器均有所增加,则表明托管堆中的内存消耗在增长。