转自:http://www.cnblogs.com/Jessy/p/3605404.htmlhtml
(1).NET 应用程序中的内存程序员
您大概已经知道,.NET 应用程序中要使用多种类型的内存,包括:堆栈、非托管堆和托管堆。这里咱们须要简单回顾一下。正则表达式
以运行库为目标的代码称为托管代码,而不以运行库为目标的代码称为非托管代码。算法
在运行库的控制下执行的代码称做托管代码。相反,在运行库以外运行的代码称做非托管代码。COM 组件、ActiveX 接口和 Win32 API 函数都是非托管代码的示例。数据库
COM/COM++组件,ActiveX控件,API函数,指针运算,自制的资源文件...这些的非托管的,其它就是托管的.在CLR上编译运行的代码就是托管代码。 非CLR编译运行的代码就是非托管代码 。非托管代码用dispose free using 释放 。即便在拥有GC的托管堆上,也有可能发生内存泄漏!编程
堆栈 堆栈用于存储应用程序执行过程当中的局部变量、方法参数、返回值和其余临时值。堆栈按照每一个线程进行分配,并做为每一个线程完成其工做的一个暂存区。垃圾收集器并不负责清理堆栈,由于为方法调用预留的堆栈会在方法返回时被自动清理。可是请注意,垃圾收集器知道在堆栈上存储的对象的引用。当对象在一种方法中被实例化时,该对象的引用(32 位或 64 位整型值,取决于平台类型)将保留在堆栈中,而对象自身却存储于托管堆中,并在变量超出范围时被垃圾收集器收集。windows
非托管堆 非托管堆用于运行时数据结构、方法表、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; using System.Threading; 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-acb8519e62c9</Id> <FirstName>John</FirstName> <LastName>Doe</LastName> </Person>
有时有必要在不改变类名称的前提下改变根元素的名称。(要与现有架构兼容可能须要根元素名称。)所以 Person 可能须要被序列化为 <PersonInstance>。XmlSerializer 构造函数可以很方便地被重载,将根元素名称做为第二参数,以下所示:
XmlSerializer serializer = new XmlSerializer(typeof(Person), new XmlRootAttribute("PersonInstance"));
当应用程序开始对 Person 对象进行序列化/反序列化时,一切运转正常,直至引起 OutOfMemoryException。对 XmlSerializer 构造函数的重载并不会对动态生成的程序集进行缓存,而是在每次实例化新的 XmlSerializer 时生成新的临时程序集。这时应用程序以临时程序集的形式泄漏非托管内存。
要修复该泄漏,请在类中使用 XmlRootAttribute 以更改序列化类型的根元素名称:
[XmlRoot("PersonInstance")] public class Person { // code }
若是直接将属性赋予类型,则 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() 方法定义。
5.总结
以上已经就 .NET 应用程序中可以致使内存泄漏或内存消耗过分的各类问题进行了讨论。虽然 .NET 可减小您对内存方面的关注程度,可是您仍必须关注应用程序的内存使用状况,以确保应用程序高效正常运行。虽然应用程序被托管,但这并不意味着您能够依靠垃圾收集器就能解决全部问题而将良好的软件工程实践束之高阁。虽然在应用程序的开发和测试阶段,您必须对其内存性能进行持续不断的监视。可是这样作很是值得。要记住,只有让用户满意才称得上是功能良好的应用程序。
关于.NET有一个鲜有人言及的问题,它和使用动态代码生成有关。简而言之,在XML序列化、正则表达式和XLST转换中用到的动态代码生成功能会引发内存泄漏。
尽管公共语言运行时(Common Language Runtime,CLR)能卸载整个应用程序域(App Domain),可是它没法卸载个别的Assemblies。代码生成依赖于建立临时Assemblies。一般这些Assemblies会被加载进主应用程序域中,这也就是说,不到应用程序退出时,它们都没法被卸载。
对于诸如XML序列化的库来讲,这个问题并不大。一般,一个给定类型的序列化代码都会缓存起来,这样应用程序则被限制在每类型只有一个临时Assembly。但有些XMLSerializer的重载没有使用缓存。假如开发人员使用了它们,又没有提供在必定程度的应用程序级别的缓存,那么随着本质上相同的代码的新实例不断被加载到内存中,内存将会慢慢发生泄漏。