前言:对于不少的C#程序员来讲,常常会不多去关注其内存的释放,他们认为C#带有强大的垃圾回收机制,全部不肯意去考虑这方面的事情,其实不尽然,不少时候咱们都须要考虑C#内存的管理问题,不然会很容易形成内存的泄露问题。javascript
尽管.NET运行库负责处理大部份内存管理工做,但C#程序员仍然必须理解内存管理的工做原理,了解如何高效地处理非托管的资源,才能在很是注重性能的系统中高效地处理内存。
C#编程的一个优势就是程序员没必要担忧具体的内存管理,垃圾回收器会自动处理全部的内存清理工做。用户能够获得近乎像C++语言那样的效率,而没必要考虑像C++中复杂的内存管理工做。但咱们仍须要理解程序在后台如何处理内存,才有助于提升应用程序的速度和性能。java
该系统把程序可用的内存地址映射到硬件内存中的实际地址上,在32位处理器上的每一个进程均可以使用4GB的硬件内存(64位处理器更大),这个4GB的内存包含了程序的全部部分(包括可执行代码、代码加载的全部DLL、程序运行时使用的全部变量的内容)
这个4GB的内存称为虚拟地址空间,或虚拟内存。其中的每一个存储单元都是从0开始排序的。要访问存储在内存的某个空间中的一个值,就须要提供表示该存储单元的数字。编译器负责把变量名转换为处理器能够理解的内存地址。程序员
值类型和引用类型在C#中的数据类型分为值类型和引用类型,对他们使用了不一样但又类似的内存管理机制。数据库
在进程的虚拟内存中,有一个区域称为栈。C#的值类型数据、传递给方法的参数副本都存储在这个栈中。在栈中存储数据时,是从高内存地址向低内存地址填充的。
操做系统维护一个变量,称为栈指针。栈指针为当前变量所占内存的最后一个字节地址,栈指针会根据须要随时调整,它老是会调整为指向栈中下一个空闲存储单元的地址。当有新的内存需求时,就根据当前栈指针的值开始往下来为该需求分配足够的内存单元,分配完后,栈指针更新为当前变量所占内存的最后一个字节地址,它将在下一次分配内存时调整为指向下一个空闲单元。
如:int a= 10;
声明一个整型的变量须要32位,也就是4个字节内存,假设当前栈指针为89999,则系统就会为变量a分配4个内存单元,分别为89996~89999,以后,栈指针更新为89995double d = 20.13;
//须要64位,也就是8个字节内存,存储在89988~89995编程
栈的工做方式是先进后出(FIFO):在释放变量时,老是先释放后面声明的变量(后面分配内存)。windows
引用类型对象的引用存储在栈中(占4个字节的空间),而它的实际数据存储在主托管堆或大对象堆上,托管堆是可用的4GB虚拟内存中的另外一个内存区域。
大对象堆:在.NET下,由于压缩较大对象(大于85000个字节)很影响性能,因此为它们分配了本身的托管堆。.NET垃圾回收器不对大对象堆执行压缩过程。
如:Person arabel= new Person();
声明变量arabel时,在栈上为该变量分配4个字节的空间以存储一个引用,new运算符为对象Person对象在堆上分配空间,而后把该空间的地址赋给变量arabel,而构造函数则用来初始化。ruby
.NET运行库为了给对象arabel分配空间,须要搜索堆,选取第一个未使用的且足够容纳对象全部数据的连续块。但垃圾回收器程序在回收堆中全部无引用的对象后,会执行压缩操做,即:把剩下的有用对象移动到堆的端部,挨在一块儿造成一个连续的内存块,并更新全部对象的引用为新地址,同时更新堆指针,方便为下一个新对象分配堆空间。网络
通常状况下,垃圾回收器在.NET运行库认为须要它时运行。System.GC
类是一个表示垃圾回收器的.NET类,能够调用System.GC.Collect()
方法,强迫垃圾回收器在代码的某个地方运行。
当代码中有大量的对象刚刚取消引用,就比较适合调用垃圾回收器,但不能保证全部未引用的对象都能从堆中删除。
垃圾回收器运行时,它实际上会下降程序的性能,由于在它执行期间,将会暂停应用程序的其它全部线程。框架
托管堆分为几个部分:第0代,第1代,第2代
全部新对象都被分配在第0代部分,在给新对象分配堆空间时,若是超出了第0代对应的部分的容量(),或者调用了GC.Collect()方法,就会开始进行垃圾回收。
每当垃圾回收器执行压缩时,第0代部分留下来的对象将会被移动到第1代上,此时第0代部分就变成空,用来放置下一个新对象。
相似的,当第一代满时,也会进行压缩,剩下对象移到下一代。
托管堆有一个堆指针,功能和栈指针相似。ide
使用.Net框架开发程序的时候,咱们无需关心内存分配问题,由于有GC这个大管家给咱们料理一切。C#中栈是编译期间就分配好的内存空间,所以你的代码中必须就栈的大小有明确的定义;堆是程序运行期间动态分配的内存空间,你能够根据程序的运行状况肯定要分配的堆内存的大小
C#程序在CLR上运行的时候,内存从逻辑上划分两大块:栈,堆。这俩基本元素组成咱们C#程序的运行环境
栈一般保存着咱们代码执行的步骤,如 AddFive()
方法,int pValue
变量,int result
变量等。而堆上存放的则可能是对象,数据等。咱们能够把栈想象成一个接着一个叠放在一块儿的盒子。当咱们使用的时候,每次从最顶部取走一个盒子。栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走(called a Frame:调用帧),接着下一个。
堆则否则,像是一个仓库,储存着咱们使用的各类对象等信息,跟栈不一样的是他们被调用完毕不会当即被清理掉(等待垃圾回收器来清理)。
栈内存无需咱们管理,也不受GC管理。当栈顶元素使用完毕,立马释放。而堆则须要GC(Garbage collection:垃圾收集器)清理。
当咱们的程序执行的时候,在栈和堆中分配有四种主要的类型:值类型,引用类型,指针,指令。
System.ValueType
的类型被称为值类型,bool byte char decimal double enum float int long sbyte short struct uint ulong ushort`System.Object
, class interface delegate object string
指针:在内存区中,指向一个类型的引用,一般被称为“指针”,它是受CLR( Common Language Runtime:公共语言运行时)管理,咱们不能显式使用。指针在内存中占一块内存区,它自己只表明一个内存地址(或者null),它所指向的另外一块内存区才是咱们真正的数据或者类型。
值类型和指针老是分配在被定义的地方,他们不必定被分配到栈上,若是一个值类型被声明在一个方法体外而且在一个引用类型中,那它就会在堆上进行分配。
栈(Stack),在程序运行的时候,每一个线程(Thread)都会维护一个本身的专属线程堆栈。
当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,而后经过JIT即时编译并把结果(通常是本地CPU指令)放在栈顶。CPU经过总线从栈顶取指令,驱动程序以执行下去。
当程序须要更多的堆空间时,GC须要进行垃圾清理工做,暂停全部线程,找出全部不可达到对象,即无被引用的对象,进行清理、压缩。并通知栈中的指针从新指向地址排序后的对象。
有了垃圾回收器,意味着咱们只要让再也不须要的对象的全部引用都超出做用域,并容许垃圾回收器在须要时释放内存便可。
原则:在.net中,没有必要调用Dispose的时候,你就不要调用它(垃圾回收器运行时会占用/阻塞主线程)。
可是,垃圾回收器不知道如何释放非托管的资源(如文件句柄、网络链接、数据库链接)。
在定义一个类时,有两种机制来自动释放非托管的资源:(更保险的作法是同时使用两种机制,防止忘记调用Dispose()
方法)
System.IDiposable
接口,实现Dispose()
方法;C#编译器在编译析构函数时,它会隐式地把析构函数编译为等价于Finalize()
方法,从而确保执行父类的Finalize()
方法。
定义方式以下:析构函数无返回值、无参数、无访问修饰符
class MyClass { ~MyClass() { } } //如下版本是编译析构函数实际调用的等价代码: protected override void Finalize() { try { //释放自身资源 } finally { base.Finalize(); } }
因为C#使用垃圾回收器的工做方式,没法肯定C#对象的析构函数什么时候执行。
定义了析构函数的对象须要通过两次垃圾回收处理才能被销毁(第二次调用析构函数时才真正删除对象),而没有定义析构函数的对象反而只须要一次处理便可删除。
若是频繁使用析构函数,并且执行长时间的清理任务,会严重影响性能。
因此,推荐经过为类实现System.IDisposable
接口,实现Dispose()
方法,来替代析构函数。IDisposable
接口定义的模式为释放非托管资源提供了肯定的机制,并避免了对垃圾回收器依赖的问题。IDisposable
接口声明了Dispose()
方法,无参数,无返回值。能够为Dispose()方法实现代码来显式地释放由对象直接使用的全部非托管资源,并在全部也实现IDisposable
接口的封装对象中调用Dispose()
方法。这样,该方法能够能够精确地控制非托管资源的释放。
注意:若是在Dispose()
方法调用以前的运行代码抛出了异常,则该方法就执行不到了,因此应该使用try...finally
,并把Dispose()
方法放在finally
块内,以确保它的执行。以下:
Person person = null; //假设Person类实现了IDisposable接口 try { person = new Person(); } finally { if(person != null) { person.Dispose(); } }
C#提供了using
关键字语法,能够确保在实现了IDisposable
接口的对象的引用超出做用域时,在该对象上自动调用Dispose()
方法,以下:
using ( Person person = new Person() ) { ..... }
using语句后面是一对"()",其中是引用变量的声明和实例化,该语句是其中的变量放在随后的语句块中,而且在变量超出做用域时,即便抛出异常,也会自动调用Dispose()
方法。
而后,在须要捕获其它异常时,使用try...finally
的方式就会比较清晰。而经常为Dispose()
方法定义一个包装方法Close()
,这样显得更清晰明了(Close()方法内仅调用Dispose()
方法)
为了防止忘记调用Dispose()
方法,更保险的作法是同时实现两种机制:即实现IDisposable
接口的Dispose()
方法,也定义析构函数。
摘要:C#程序中的Dispose方法,一旦被调用了该方法的对象,虽然尚未垃圾回收,但实际上已经不能再使用了。
先了解一下C#程序(或者说.NET)中的资源分类。简单的说来,C#中的每个类型都表明一种资源,而资源又分为两类:
非托管资源:不受CLR管理的对象,windows内核对象,如文件、数据库链接、套接字、COM对象等;
毫无例外地,若是咱们的类型使用到了非托管资源,或者须要显式释放的托管资源,那么,就须要让类型继承接口IDisposable
。这至关因而告诉调用者,该类型是须要显式释放资源的,你须要调用个人Dispose
方法。
不过,这一切并不这么简单,一个标准的继承了IDisposable
接口的类型应该像下面这样去实现。这种实现咱们称之为Dispose模式:
public class SampleClass : IDisposable { //演示建立一个非托管资源 private IntPtr nativeResource = Marshal.AllocHGlobal(100); //演示建立一个托管资源 private AnotherResource managedResource = new AnotherResource(); private bool disposed = false; /// <summary> /// 实现IDisposable中的Dispose方法,用于手动调用 /// </summary> public void Dispose() { //必须为true Dispose(true); //通知垃圾回收机制再也不调用终结器(析构器)由于咱们已经本身清理了,不必继续浪费系统资源 //即:从等待终结的Finalize队列中移除this GC.SuppressFinalize(this); } /// <summary> /// 不是必要的,提供一个Close方法仅仅是为了更符合其余语言(如C++)的规范 /// </summary> public void Close() { Dispose(); } /// <summary> /// 必须,以备程序员忘记了显式调用Dispose方法 /// </summary> ~SampleClass() { //必须为false,跳过托管资源的清理,只手动清理非托管的资源,垃圾回收器会自动清理托管资源 Dispose(false); } /// <summary> /// 非密封类修饰用protected virtual /// 密封类修饰用private /// </summary> /// <param name="disposing"></param> protected virtual void Dispose(bool disposing) { if (disposed) { return; } if (disposing) { // 清理托管资源 if (managedResource != null) { managedResource.Dispose(); managedResource = null; } } // 清理非托管资源 if (nativeResource != IntPtr.Zero) { Marshal.FreeHGlobal(nativeResource); nativeResource = IntPtr.Zero; } //让类型知道本身已经被释放 disposed = true; } public void SamplePublicMethod() { //确保在执行对象的任何方法以前,该对象可用(未被释放) if (disposed) { throw new ObjectDisposedException("SampleClass", "SampleClass is disposed"); } //在这里可使用对象 } }
在Dispose模式中,几乎每一行都有特殊的含义。
在标准的Dispose模式中,咱们注意到一个以~开头的方法:
/// <summary> /// 必须,以备程序员忘记了显式调用Dispose方法 /// </summary> ~SampleClass() { //必须为false Dispose(false); }
这个方法叫作类型的终结器。提供终结器的所有意义在于:咱们不能奢望类型的调用者确定会主动调用Dispose方法,基于终结器会被垃圾回收器调用这个特色,终结器被用作资源释放的补救措施。
一个类型的Dispose方法应该容许被屡次调用而不抛异常。鉴于这个缘由,类型内部维护了一个私有的布尔型变量disposed:private bool disposed = false;
在实际处理代码清理的方法中,加入了以下的判断语句:
if (disposed) { return; } //省略清理部分的代码,并在方法的最后为disposed赋值为true disposed = true;
这意味着类型若是被清理过一次,则清理工做将再也不进行。
应该注意到:在标准的Dispose模式中,真正实现IDisposable
接口的Dispose方法,并无实际的清理工做,它实际调用的是下面这个带布尔参数的受保护的虚方法:
/// <summary> /// 非密封类修饰用protected virtual /// 密封类修饰用private /// </summary> /// <param name="disposing"></param> protected virtual void Dispose(bool disposing) { //省略代码 }
之因此提供这样一个受保护的虚方法,是为了考虑到这个类型会被其余类继承的状况。若是类型存在一个子类,子类也许会实现本身的Dispose模式。受保护的虚方法用来提醒子类必须在实现本身的清理方法的时候注意到父类的清理工做,即子类须要在本身的释放方法中调用base.Dispose方法。
还有,咱们应该已经注意到了真正撰写资源释放代码的那个虚方法是带有一个布尔参数的。之因此提供这个参数,是由于咱们在资源释放时要区别对待托管资源和非托管资源。
在供调用者调用的显式释放资源的无参Dispose方法中,调用参数是true:
public void Dispose() { //必须为true Dispose(true); //其余省略 }
这代表,这个时候代码要同时处理托管资源和非托管资源。
在供垃圾回收器调用的隐式清理资源的终结器中,调用参数是false:
~SampleClass()
{
//必须为false Dispose(false); }
这代表,隐式清理时,只要处理非托管资源就能够了。
那么,为何要区别对待托管资源和非托管资源。在认真阐述这个问题以前,咱们须要首先弄明白:托管资源须要手动清理吗?不妨先将C#中的类型分为两类,一类继承了IDisposable接口,一类则没有继承。前者,咱们暂时称之为非普通类型,后者咱们称之为普通类型。
非普通类型由于包含非托管资源,因此它须要继承IDisposable接口,可是,这个包含非托管资源的类型自己,它是一个托管资源。因此说,托管资源须要手动清理吗?这个问题的答案是:托管资源中的普通类型,不须要手动清理,而非普通类型,是须要手动清理的(即调用Dispose方法)。
Dispose模式设计的思路基于:若是调用者显式调用了Dispose方法,那么类型就该循序渐进为本身的因此资源所有释放掉。若是调用者忘记调用Dispose方法,那么类型就假定本身的全部托管资源(哪怕是那些上段中阐述的非普通类型)所有交给垃圾回收器去回收,而不进行手工清理。理解了这一点,咱们就理解了为何Dispose方法中,虚方法传入的参数是true,而终结器中,虚方法传入的参数是false。
在CLR托管应用程序中,存在一个根的概念,类型的静态字段、方法参数以及局部变量均可以做为根存在(值类型不能做为根,只有引用类型的指针才能做为根)。垃圾回收器会沿着线程栈上行检查根,若是发现该根的引用为空,则标记该根为可被释放。而JIT编译器是一个通过优化的编译器,不管咱们是否为变量赋值为null,该语句都会被忽略掉,在咱们将项目设置为Release模式下,该语句将根本不会被编译进运行时内。可是,在另一种状况下,却要注意及时为变量赋值为null。那就是类型的静态字段。并且,为类型对象赋值为null,并不意味着同时为该类型的静态字段赋值为null:当执行垃圾回收时,当类型的对象被回收的时候,该类型的静态字段并无被回收(由于静态字段是属于类的,它往后可能会被该类型的其它实例继续使用)。实际工做中,一旦咱们感受到本身的静态引用类型参数占用内存空间比较大,而且使用完毕后再也不使用,则能够马上将其赋值为null。这也许并没必要要,但这绝对是一个好习惯。