C# 资源释放

C# 内存管理(资源释放):

1 内存管理:

值数据类型

  • 首先 Windows 是使用一个虚拟寻址系统,该系统吧程序可用的内存地址映射到硬件内存中的实际地址上,这些任务完全由 Windows系统在后台管理。其实际的结果是,32位处理器上的每个进程都可以使用 4GB 的内存无论计算机实际由多少内存(在64位处理器上,这个数字会更大)。这个4GB的内存包含了程序的所有部分,包括可执行代码,代码加载的所有 DLL ,以及程序运行时所用变量内容。这个4GB 内存称为虚拟地址空间,或虚拟内存。
  • 内存中的每个存储单元都是从0开始往上排序的。要访问存储在内存空间中的某个值时,就需要提供标识该存储单元的数字。在任何复杂的高级语言中,如 C++,C# ,Java 等,编译器负责把可以理解的变量名称转换为处理器可以理解的内存地址。
  • 在处理器的虚拟内存中,有一个区域称为栈。栈存储不是对象成员的值数据类型。另外在调用方法时,也使用栈存储传递给方法的所有参数的副本。为了理解栈的工作原理,需要注意在C#中变量的作用域。示例:
{
	int a;
	// do something
	{
		int b;
		//do something
	}
}

首选声明了 a ,在内部代码块中声明了 b 。程序运行时,内部代码块终止,b 就超出了作用域,最后 a 超出了 作用域。所以 b 的生存期完全包含在 a 的生存期中。在释放变量时,其顺序总是与给它们分配内存的顺序相反,这就是栈的工作方式。 还要注意 b 在另一个代码块中 。因此它包含在另一个作用域中,这称为块级作用域或结构作用域

  • 我们不知道栈具体在地址空间的什么地方,在进行C# 开发是不需要知道的。栈指针(操作系统维护的一个地址变量)表示栈中下一个空闲存储单元的地址。程序在第一次进行运行时,栈指针指向为栈保留的内存块末尾。栈指针是向下填充的,即从高内存地址向低内存地址填充。当数据入栈后,栈指针就会随之调整,保证始终指向下一个空闲的内存单元。C# 资源释放_第1张图片
    如上图所示的栈:声明在代码中声明变量:存储一个整数和双浮点数
  {
  	int n=10;
  	double d=300.01;
  	//do calculations.
  }

n 变量进入作用域,赋值为 10,这个值存放在存储单元 7999996 ~ 7999999 上,这4个字节就在栈指针所指的空间下面。变量的数据类型,决定了栈指针移动的步长。int 类型为4字节,为了容纳 int 类型的数据,应从栈指针对应的值(地址)中减去4 ,所以现在指向的位置是 7999996,而下一个空闲内存单元是 7999995。下一行代码声明了一个 double 类型的变量 d ,初始化为 300.01。double 类型占 8 个字节。所以在栈上的存储单元为 7999988 ~ 7999995 上,栈指针对应的值减去 8 再次指向栈上的下一个空闲存储单元。
当变量 b 超出作用域时,运行库(CLR)就知道不需要这个变量了。因为变量的生存期总是嵌套的,当 d 在作用域时,无论发生什么情况,都可以保证栈指针总是指向存储 d 的空间。为了从内存中删除这个变量,应该给栈指针的值递增 8 ,指向 变量 d 末尾相邻的空间。当 变量 a 也超出作用域时,栈指针应该再次递增 4 。从栈中删除 变量 a 和 d 后,如果此时在作用域中由放如另一个变量,从地址 7999999 开始的存储单元就会被覆盖掉。
如果编译器遇到 int i,j; 这样的代码,则这两个变量进入作用域的顺序是不确定的。两个变量是同时声明的,也是同时超出作用域。此时,变量以什么顺序从内存中删除就不重要了。编译器会确保先放入内存中的那个变量后删除,这样就会保证该规则不会与变量的生存期冲突。

2 引用数据类型:
  • 尽管栈有非常高的性能,但是没有灵活到用于所有变量。有时希望使用一个方法分配内存,来存储一些数据,并且在方法退出后很长一段时间内数据任然是可用的。这就需要用 new 运算符来请求分配空间才有这种可能性 – 例如对所有的引用类型。此时就需要使用托管堆。
  • C#中 托管堆在垃圾回收器(GC)的控制下工作,与传统的堆相比有显著的优势。托管堆是处理器的可用 4GB 内存中的 另一个区域。要了解堆的工作原理和如何为引用类型分配内存,示例:
 void DoWork()
 {
 	Custome  arable;  
 	arable= new Custome();
	Custome  otherCustome2= new EahancedCustome();   
 }

上面代码:假定两个类 Custome 和 EahancedCustome。EahancedCustome 扩展了 Custome 类。 首先 Custome arable; 声明了一个 Custome 对象的引用 arable(变量),在栈上给这个引用分配存储空间,但这只是只是引用(一个地址),而不是实际的 Custome 对象。arable 引用占用4个字节存储引用地址。 然后下一行 arable= new Custome(); 代码完成以下操作:首先,它分配堆上的内存,存储 Custome 对象(一个真正的对象,而不是地址),然后把变量 arable 的值 重新分配给新对象 Custome 的内存地址。
Custome 对象的值放在堆中,而栈中存放的只是指向 堆存储空间的地址。

C# 资源释放_第2张图片
C# 的 new 操作符导致 CLR 执行下列步骤:

  1. 计算类型的字段(以及从基类型继承的字段)所需要的字节数。
  2. 加上对象的开销所需要的字节数。每个字节数都有两个开销字段:类型对象指针和同步块索引。对于32位应用程序这两个字段各自需要32位,所以每个对象增加8字节。对于64位应用程序,这两个字段各自需要64位,所以每个字段怎加16字节。
  3. CLR检查区域中是否有分配对象所需要的字节数。如果托管堆有足够的可用空间,就在 NextObjPrt 指针指向的地址放入对象,为对象分配的字节会被清零。接着调用类型的构造器(为 this 参数传递 NextObjPrt),new 操作符返回对象的引用。就在返回这个引用之前,NextObjPrt 指针的值会加上对象占用的字节数来得到一个新值,即下一个对象放入托管堆。
3 垃圾回收算法:GC

应用程序 new 操作符创建对象时,如果内有足够的地址空间来分配对象,发现空间不足时,CLR就会执行垃圾回收。

一般情况下,垃圾回收器在 .NET 运行库任务需要进行垃圾回收时运行。可以调用 System.GC.Collect() 方法,强迫垃圾回收器在代码的某个地方运行,System.GC 类是一个 .NET 中表示垃圾回收的类,Collect() 方法启动一个垃圾回收过程。但是 GC 类使用的场合很少。例如:代码中有大量的对象刚刚取消引用,就适合调用垃圾回收器。但是,垃圾回收器的逻辑不能保证在一次垃圾回收过程中,所有未引用的对象都从堆中删除。

CLR 的 GC 是基于代的垃圾回收器(generational garbage collector), 它对代码做出以下几点假设:

  • 对象越新,生存期越端
  • 对象越老,生存期越长
  • 回收堆的一部分,速度快于回收整个堆

详情:
托管堆在初始化时不包含对象。添加到堆的对象称为第 0 代对象。简而言之,第 0 代对象就是那些新构造的对象,垃圾回收器从未检查过它们。如下图:新启动的程序分配 5 个对象 (A 到 E)程序运行一会 对象 C 和 E 变得不可达(失去引用)

C# 资源释放_第3张图片
CLR 初始化时为 第 0 代 对象选择一个预算容量(以 KB 为单位)。如果分配一个新对象造成第 0 代超过预算,就必须启动一次垃圾回收。假设 A 到 E 刚好用完 第 0 代空间,那么分配对象 F 就必须启动垃圾回收。垃圾回收器判断对象 C 和 E 是垃圾,所以会压缩对象 D ,使之与对象 B 相邻。在垃圾回收中存活的对象(A ,B,和D)现在称为第 1 代对象。第 1 代对象已经经历了一次垃圾回收器的检查。如下图:
C# 资源释放_第4张图片
一次垃圾回收后,第 0 代就不包含任何对象了。和前面一样,新对象会分配到第 0 代中。如果程序继续运行,并新分配了对象 F 到 K 。另外 ,随着程序再次运行,对象 B ,H 和 J 变得不可达(失去引用),它们的内存将在某一时刻回收。如下图:
C# 资源释放_第5张图片
现在,假定第 0 代空间已满, 分配新对象L 会造成第 0 代超出预算,就必须启动垃圾回收。开始启动垃圾回收时,垃圾回收器就必须检查哪些代。前面说过,CLR 初始化时会为第0代对象选择预算。事实上,它还必须为第 1 代 选择预算。
开始一次垃圾回收时,垃圾回收器还会检查第 1 代 占用多少内存。在本例中由于第1 代占用的内存远少于预算,所以垃圾回收器只检查第 0 代中的对象。回顾上面基于代 垃圾回收器做出的假设,第一是越新的对象活的越短。因此,第 0 代包含更多垃圾的可能性很大,能回收更多的内存。由于忽落了第 1 代,加快了垃圾回收的速度。
显然,忽落第 1 代中的对象能提升垃圾回收器的性能。但对性能更大的提升作用的是现在不必便利托管堆中的每一个对象。如果根或对象引用了老一代的某个对象,垃圾回收器就可以忽落老对象内部的所有引用,能在更短的时间内构造好可达对象图(graph of reachable object)。当然,老对象的字段也有可能引用新对象。为了确保对老对象的已更新字段进行检查,垃圾回收器利用 JIT 编译器内部的一个机制。这个机制在对象的引用字段发生变换时,会设置一个对应的位标志。这样,垃圾回收器就知道自上一次垃圾回收以来。那些老对象(如果有的话)已被写入。只有字段发生变换的老对象才需要检查是否引用了第 0 代中的任何新对象。
基于代的垃圾回收器还假设越老的对象活的越长。也就是说,第 1 代对象在应用程序中很有可能是继续可达的。如果垃圾回收器检查第 1 代 对象,很有可能找不到垃圾,结果是回收不了多少内存。因此,对第 1 代进行垃圾回收很可能是浪费时间。如果真有垃圾在第 1 代中,它将留在那里。如下图:
C# 资源释放_第6张图片
如上图所示,所幸存下来的第 0 代都称为 第 1 代的一部分,由于垃圾回收器没有检查第 1 代,所以对象 B 的内存并没有被回收,即使它在上一此垃圾回收时已经不可达。同样在一此垃圾回收后,第 0 代不包含任何对象,等待分配新对象。假定程序继续运行,并分配对象 L 到 O 。另外 对象 G,L和M 变得不可达(失去引用)。此时 如下图:
C# 资源释放_第7张图片
假设再次分配对象 P 导致第 0 代超过预算,垃圾回收发生。由于第 1 代中所有对象占据的内存仍小于预算,所以垃圾回收器再次回收第 0 代 ,忽落第 1 代 中的垃圾对象(B和G),回收后如下:
C# 资源释放_第8张图片
从上面可以看出,第 1 代正缓慢增长。假定第 1 代的增长导致它的存储空间超出预算,这时,程序继续运行(因为垃圾回收刚刚完成),并分配对象 P 到 对象 S ,并使 第 0 代 对象达到它的预算容量。
如下:
C# 资源释放_第9张图片
应用程序在此分配对象 T 时, 由于第 0 代已满,所以必须进行垃圾回收。但这一次垃圾回收器发现 第 1 代占用了太多内存,以至于预算用完。由于前几次对第 0 代进行垃圾回收时,第 1 对可能有许多对象变得不可达(失去引用)。所以垃圾回收器决定检查第 1 代 和 第 0 代中的所有对象。两代都被垃圾回收后,堆的情况如下:
C# 资源释放_第10张图片
和之前一样,经过垃圾回收后,第 0 代的幸存者被提升为 第 1 代,第 1 代的幸存者被提升为第2代,第 0 代 再次空出,准备接受新对象。第 2 代中的对象经过2此或更多此检查。虽然到目前位置发生过多次垃圾回收,但只有第 1 代 超出预算时才会检查第 1 代中的对象。而再次之前,一般已经对第 0 代进行了好几次垃圾回收。

托管堆只支持三代:第 0 代,第 1 代,第 2 代。没有第 3 代。CLR 初始化会为每一代选择预算。然而 CLR的垃圾回收器是自调节的。这意味着垃圾回收器会在执行垃圾回收的过程中了解应用程序的行为。例如:假定应用程序构造了许多对象,但每个对象用的时间都很短。在这种情况下,对第 0 代的垃圾回收会回收大量内存。事实上,第 0 代所有对象都可能被回收。如果垃圾回收器发现在回收 0 代后存活下来的对象很少,就可能减少第 0 代的预算。已分配空间的减少意味着垃圾回收更频繁地发生,但是垃圾回收器每次做的事情也减少了,这减少了进程的工作集。事实上,如果第 0 代中的所有对象都是垃圾,垃圾回收就不必压缩(移动)任何内存;只须让 NextObjPrt 指针指回第 0 代的起始处即可。这样回收垃圾更让快!!!
另一方面,如果垃圾回收器回收了第 0 代,发现还有很多对象存活,没有对少内存被回收,就会增大第 0 代的预算。现在,垃圾回收的次数将减少,但每次进行垃圾回收时,回收的内存要多得多。但是,如果内有回收到足够的内存,垃圾回收器会执行一次完整的回收,如果还不够,就会抛出 OutOfMemoryException 异常。

到目前为止,只是讨论了每次垃圾回收后如何动态调整第 0 代的预算。但垃圾回收器还用类似的启发式算法调整第 1 代 和 第 2 代 的预算。这些代被垃圾回收时,垃圾回收器会检查有多少内存被回收,以及多少对象幸存。基于这些结果,垃圾回收器可能增大或减少这些代的预算,从而提升应用程序的总体性能。最终实现垃圾回收器会根据应用程序要求的内存负载来字段优化。

4 垃圾回收的触发条件:
  1. 代码显示条用 System.GC 的静态 Collect 方法 代码可以显式请求 CLR 执行回收,但微软强烈反对这种请求,但是有时需要。
  2. Windows 报告低内存情况 CLR 内部使用 WIN32 函数 CreateMemoryResourceNotification 和 QueryMemoryResourceNotification 监视系统的总体内存使用情况。如果 Windows 报告低内存,CLR将强制垃圾回收亦释放失去引用的对象,减小进程工作集。
  3. CLR 正在卸载 AppDomina 一个 AppDomain 卸载时,CLR认为其中一切都不是根,所以执行涵盖所有代的垃圾回收。
  4. CLR正在关闭 CLR 在进程正常终止时关闭。关闭期间,CLR认为进程中一切都不是根。对象有机会进行资源清理,但 CLR 不会视图压缩或释放内存。整个进程要终止了,Windows 将回收进程的全部内存。
5 显式释放资源需继承接口 IDisposable:

C# 中的每个类型都代表一种资源,而资源有分为两类:

  • 托管资源:由 CLR 管理分配和释放的资源。即 从 CLR 里 NEW 出来的对象。
  • 非托管资源:不受CLR 管理的对象,如 Windows 内核对象,或者文件,数据库连接,套接字,COM对象等。
public class ClassDome:IDisposable
    {
        public int Count { get; set; }
        public string Str { get; set; }
        private IntPtr handle;

        public StreamReader reader =null;

        private bool isDisposed = false;

        public ClassDome(IntPtr handle)
        {
            Count = 1;
            Str = "AAA";
            this.handle = handle;
            reader = new StreamReader(@"D:\T.txt", Encoding.UTF8);
        }

        /// 
        /// 析构函数
        /// 
        ~ClassDome()
        {
            this.Dispose(false);
        }
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if(!isDisposed)
            {
                if(disposing)
                {
                    //通过调用托管对象的Dispose()方法来清除托管对象
                    this.Str = null;
                    reader.Close();
                    reader.Dispose();
                }
                //清理非托管对象
                CloseHandle(handle);
                handle = IntPtr.Zero;
            }
            this.isDisposed = true;
        }

        #region 清理非托管资源
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle); 
        #endregion

        #region 
        [DllImport("kernel32.dll", EntryPoint = "SetProcessWorkingSetSize")]
        public static extern int SetProcessWorkingSetSize(IntPtr process, int minSize, int maxSize);
        /// 
        /// 释放内存
        /// 
        public static void ClearMemory()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            if (Environment.OSVersion.Platform == PlatformID.Win32NT)
            {
                ClassDome.SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
            }
        } 
        #endregion
    }

你可能感兴趣的:(C#,c#,资源释放)