《Effective C#》读书笔记——了解.NET内存管理机制<.NET资源管理>
我们知道C#是一门虚拟机语言,在C#编译器首先将C#代码编译成IL代码,运行程序时CLR(Common Language Runtime,公共语言运行时)通过调用JIT(just-in-time Compiler,即时编译器)来将IL代动态即时编译成可执行的机器码。在CLR中有一个非常重要的概念:CLR GC(Garbage Collector,垃圾收集器),GC自动为我们的应用程序进行内存管理的分配和释放,所以在.NET应用程序中一般很少出现内存泄露、悬挂指针、未初始化指针等问题。同时这也是C#这样的虚拟机语言和C/C++等本地语言最大的不同之处。
但是GC也不是万能的,我们也需要自己做一些清理工作,管理那些非托管的对象,它们通常是包装操作系统资源的对象,例如,文件句柄、窗口句柄或网络连接。
这是因为外部的资源已经超出了运行时(虚拟机)的能力范围,而这些非托管的资源是由操作系统来掌控的。
同时如果使用了事件处理函数和委托在对象之间建立了链接,也可能会造成对象超过你预期的长时间驻留在内存中同时类似于LINQ的这样的在只有请求结果是才真正执行的查询也会导致同样的问题(因为查询将把需要绑定的局部变量使用闭包封装起来,而绑定对象只有在查询结果离开作用域时才会被释放掉)。可以看出在托管环境中开发应用程序,了解CLR内存的管理机制,对于我们写出更富有效率和健壮的代码是非常有益的。
阅读目录:
1.标记并压缩算法(Mark and Compact)
GC使用"标记并压缩"(Mark and Compact)算法分析程序运行对象的关系,将不可达部分对象从内存中清理掉。GC会从应用程序的根对象开始,通过对对象树形结构遍历来判定一个对象是否可达,而并不是向COM那样跟踪每个对象的引用。
我们可以假设有一个EntitySet,是从数据库中读取得到的一系列对象的集合。每个Entity对象都可能包含对其他Entity对象的引用,还可能包含对其他Entity的连接。类似于关系型数据库中的实体集合一样,这些链接和引用也可能是循环的。每个EntitySet中都包含了一系列对象图之间的复杂引用。但是这些并不需要我们担心,GC会替我们很好的完成这个任务。GC通过判断某一个对象(图)是否为垃圾来简化这一操作——当应用程序不需要某一个对象时,将不会保留这个对象的引用。GC会将那些没有被任何对象之间或间接引用的对象判定为垃圾。
GC在其专门的线程中运行,GC不但自动为程序清理不在使用的内存,还会在每次运行时压缩托管堆。因此内存中空余的空间会是一片连续的内存。
下面图为一个托管堆在一次垃圾收集前后的状态:
我们可以看到GC不但清理不在使用的内存,还会移动内存中的其他对象,以便压缩正在使用的内存,并提高可用的内存空间。
2.清理非托管资源
现在我们知道:托管堆上的内存管理是GC的职责。但是,非托管资源的内存管理则需要由我们开发者自己来负责。.NET为我们提供了两种控制非托管资源生命周期的机制:终结器(finalizer)和IDisposable接口。
2.1 (终结器)finalizer
通过调用Finalize()方法通知GC回收该对象使用的内存时同时清理其非托管资源。终结器是一种防御性手段,让你的对象不管如何都可以被释放掉其中使用的非托管资源。终结器由GC调用,调用将发生在对象成为垃圾之后的某个时间(我们并不能准确的知道其发生的具体时间,只是知道这将发生在对象不可达之后)。这和C++相比区别很大,有经验的C++开发者往往会在构造函数中分配关键资源,然后在析构函数中释放掉。
2.1.1 使用终结器的问题
在C#中,终结器迟早都会执行,不过其执行时间却无法预料(也不能)。终结器仅仅能够保证给定类型的对象所分配的非托管资源最终会被释放,但其执行时间确实不可预料的,因此:在我们的代码中最好避免使用终结器,也应该尽量少让代码逻辑使用到终结器。
依赖终结器还对带来性能上的损失。需要执行终结器的对象会给GC带来额外的性能开销。当GC发现某个对象属于垃圾,而该对象又需要执行终结器时,就不能将其直接从内存中移除:首先,GC将调用终结器,而终结器并不在执行垃圾收集的线程上执行。GC将把所有需要执行终结器的对象放在专门的队列中,然后让另一个线程来执行这些对象的终结器。用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。所有我们知道:使用终结器释放非托管资源,资源对象不但会在内存中停留更长时间,GC也需要额外的线程来运行。
2.1.2 GC中"代"(Generation)的概念
.NET的GC为了优化其执行,引入了“代”(Generation)的概念。这概念可以让GC更快速的找到那些更有可能是垃圾的对象。
自上一次垃圾收集以后,新创建的对象属于第0代对象。而如果经过上一次垃圾收集之后仍旧存活的对象,将成为第1代对象。两次以及两次以上的垃圾收集仍没有被销毁的对象就变成了第2代对象。
这样的分代方式是为了能将局部变量和应用程序生命周期中一直使用的对象分开对待。第0代大多属于局部变量。而成员变量和全局变量则会变成第1代对象,直至变成第2代对象。而GC通过减少检查第1代和第2代的次数来优化执行过程。大概10个周期的GC中,只有一次会同时检查第0代和第1代对象;大概100个周期的GC中,会有一次同时检查所有对象。所有,一个需要终结器的对象可能会比普通的对象多停留9个GC周期。而对于第2代对象,甚至需要100次以上的GC周期才有机会被销毁。
2.2 IDisposable接口
相较于终结器IDisposable接口能够以一种影响更小的方式及时的释放非托管资源。使用IDisposable接口释放非托管资源可以避免执行终结器给GC带来性能上的影响。实现IDisposable接口的 Dispose 方法应该释放它拥有的所有资源。 它还应该释放其基类型拥有的所有资源通过调用其父类型的 Dispose 方法。
GC自动为我们的应用程序进行内存管理的分配和释放,所以在.NET应用程序中一般很少出现内存泄露、悬挂指针、未初始化指针等问题。非托管的资源要使用终结器来保证释放。即使终结器会对程序的性能带来很大的影响,我们也必须提供终结器这是,这是为了保证不出现资源泄露的问题。但是,使用使用IDisposable接口释放非托管资源可以避免执行终结器给GC带来性能上的影响。