找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 2798|回复: 0

.Net 内存管理、资源管理(参考整理自李建忠老师)

[复制链接]
发表于 2012-2-23 20:57:47 | 显示全部楼层 |阅读模式
内存管理是一个非常重要的东西,一个好的程序员应该对内存模型和内存管理有一个好的认识。首先我们了解一下资源,资源分为两类,即托管资源和非托管资源。

托管资源:托管堆内存
非托管资源:文件句柄、数据库链接、本地内存等
这里的托管与非托管即指的是我们常说的垃圾收集器,被垃圾收集器管理的资源叫托管资源,我们常说的内存管理即指的是托管资源的管理,而资源管理即指的是非托管资源的管理。
下面我们谈谈.NET的内存管理
在分配内存的过程中,当保留的内存区域全部被用光时,这时候GC启动,通过对象图,进行扫描,找到那些不可达的对象,这些对象即为垃圾对象,然后对内存区域进行压缩,使垃圾对象被覆盖掉(这里基于一个前提:托管堆上的内存是连续分配的),由于对象的位置发生了偏移,因此需要进行指针更新。
.NET线式分配,间歇性压缩和搬移对象内存,对象地址不稳定(快、无碎片)
C++链式分配,对象地址稳定不变(慢,有碎片)
垃圾收集器的启动是不定时的,只有当内存不够使用时才会被启动,由于无内存碎片,因此C#比较适合开发服务器端应用程序。
标记压缩法的三大特点:
1.分配速度快,回收速度慢
2.确保空闲内存区域连续,避免内存碎片
3.对象地址不稳定
分代式垃圾收集:
CLR执行一个单独的线程来负责垃圾收集器,这时它会挂起当前的所有线程
分代式垃圾收集器区分代龄基于以下假设:
对象越新,其生存周期越短
对象越老,其生存周期越长
每一个托管堆对象分配一个代龄:
0代对象限额:256K
1代对象限额:2M
2代对象限额:10M
每个对象的代龄存储在对象的第一个保留字段区域
首先垃圾收集器将所有的对象分为0、1、2三代,其中大部分对象被分为0代,当0代对象越来越多,达到了内存限额256K,这时垃圾收集器启动0代收集,将那些不可达对象进行收集,那些经过0代收集仍然存活的对象,垃圾收集器会对其进行搬移,将其转为1代对象。1代和2代得收集原理和0代相似,只不过在启动1代收集时同时会启动0代收集,在启动2代收集时同时会启动0代、1代收集。
对于一些比较小的软件,垃圾收集器的二代是不会启动的,如果运行的时间非常长,二代才可能会启动,垃圾收集器的这种机制就是使那些难以回收的对象的晚一些回收,对于一些比较复杂的对象,垃圾收集器一开始就将其标记为2代对象,对于一些比较简单的对象,垃圾收集器一开始就将其标记为0代对象,这就是所谓的垃圾收集器“欺软怕硬”的精神。
代龄的内存分配是动态调整的,如果垃圾收集器发现系统频繁的调用0代、1代、2代收集器,同时它又发现你系统的内存比较大,它会把每一代的内存限额相应的调大一些,使得垃圾收集器启动的不那么频繁。这是它懒惰的表现
垃圾收集器没有提供任何一种方法只回收一个特定的对象,垃圾收集器从来不屑与去收集一个对象,因为收集一个对象的代价过高,如果我们想回收特定的某一代的对象,GC提供了一个函数
GC.Collect(),这个函数在System命名空间,其参数只能为0、1、2,如果不带参数,则强行对所有代进行垃圾回收,如果带参数,则强行对0代到指定代进行垃圾回收。这种方法是不被鼓励使用的,GetGeneration(Object),返回指定对象的当前代数。这些方法只限于实验性的使用。
资源管理(非托管资源)
GC主要负责回收内存,对于非托管资源只能进行辅助性的回收,下面我们举一个例子
using System;
class MyClass
{  
     int x;         //纯内存对象,垃圾收集器可完全回收
     int y;
     
     IntPtr handle  //资源对象,代表一个资源
     public MyClass()
     {
          handle=OpenHandle(); //获取资源
         }
    }
    class Test
    {
         public static void Main()
         {
              MyClass obj=new MyClass();
              //............
              
//............         
              
//GC启动
             }
        }

获取资源:即向Windows操作系统去请求资源,Windows操作系统会标记出当前进程所占用的资源,操作系统是以进程为单位来划分资源的,只要进程获取了资源,且进程不关闭,Windows操作系统就会一直认为这个资源被占用着,直到进程被关闭。
上面的代码中,我们定义了一个资源类(如数据访问类就是一个资源类),然后在测试函数中我们创建了一个资源类的对象,同时在其构造函数中获取了相应的资源,
下面我们通过图解说明:

当这个对象使用完以后就成为了垃圾对象,GC启动对对象进行回收,但是GC并不会对对象所使用的资源进行释放,因为你没有告诉垃圾收集器资源不用了。此时即产生了资源泄漏,直至进程关闭,其所占用的资源才会被释放。在一些小的程序中,即便资源泄漏,也不会对系统产生多大的影响,因为进程可能很快就会被关闭,但是对于一些服务器运行程序,其运行时间一般会很长,其造成的资源泄漏是不容忽视的。
下面我们对上面的代码进行改进:
using System;
class MyClass
{  
     int x;         //纯内存对象,垃圾收集器可完全回收
     int y;
     
     IntPtr handle  //资源对象,代表一个资源
     public MyClass()
     {
          handle=OpenHandle(); //获取资源
     }
     ~MyClass()  //析构器
     {
          CloseHandle(handle);  //释放资源
     }
    }
    class Test
    {
         public static void Main()
         {
              MyClass obj=new MyClass();
              //............
              
//............         
              
//GC启动
         }
    }

在这里我们在析构器中对资源进行了释放,当GC启动的时候,如果析构器不存在,GC就会直接回收对象的内存,但由于有了析构器,这时GC就不会选择直接回收对象内存了,转而去调用析构器了,而且GC在对对象进行第一次回收时只会执行析构器,不会回收内存,也就是说垃圾收集器在一次执行过程中只能做一件事情。因此对象就会变为1代对象,这就是为什么会析构器会托大对象的代龄了。
为了避免对象的代龄被托大,同时为了能够及时的释放对象所占用的资源,把释放资源的任务交给垃圾收集器是不可靠的,下面我们看一个改进的例子:
using System;
class MyClass : IDisposable
{  
     int x;         //纯内存对象,垃圾收集器可完全回收
     int y;     
     IntPtr handle  //资源对象,代表一个资源
     public MyClass()
     {
          handle=OpenHandle(); //获取资源
     }
     public void Dispose()
     {
          CloseHandle(handle)  //释放资源
          GC.SuppressFinalize(this);  //告诉系统就当我没有写析构器,避免托大代龄
         }
     ~MyClass()  //析构器
     {
          CloseHandle(handle)  //释放资源
     }
    }
    class Test
    {
         public static void Main()
         {
              MyClass obj=new MyClass();
              obj.Dispose();
              //............
              
//............         
              
//GC启动
         }
    }

在这里我们通过IDisposable接口实现了Dispose()方法,即我们在对象使用完毕以后可以自行调用Dispose()方法来实现对资源的释放,不用等待析构器去释放资源,同时我们在这个函数中告诉垃圾收集器不要调用析构器,避免代龄被托大,其实Dispose()方法可以用一个普通的方法来表示,但是,通过接口来实现资源的释放,可以让别人一眼就能看出这是一个资源类。
这里又有一个问题,既然通过Dispose()方法实现了资源的自行释放,那为什么还需要写析构器了,原因就是以防程序员在编写程序的过程中忘记了调用Dispose()方法去释放资源,这时也会造成资源的泄露,因此为了绝对的安全,还是需要将析构器写上。
上面这些也就形成了资源处理的一个设计模式,这个设计模式有一个约定,即把释放资源的方法的名字叫做Dispose,同时该方法所在的类需要实现IDisposable接口,有了这个接口以后,就可以让使用类的程序员知道该类为一个资源类,当对象用完的时候一定要调用Dispose()方法。
析构器的本质是一个Finalize方法,当我们编译完上面的代码,用Ildasm工具去查看去IL代码,就会发现析构器被编译成了一个Finalize方法。它是重写了基类的一个受保护的虚方法。
但是如果程序在执行过程中抛出了异常,导致Dispose()方法没有被执行,这时虽然有析构器确保资源被释放,但是这并不是我们的本意,因此为了确保Dispose()方法被执行,我们需要写一个异常处理的语句结构,下面我们看一看代码
using System;
class MyClass : IDisposable
{  
     int x;         //纯内存对象,垃圾收集器可完全回收
     int y;     
     IntPtr handle  //资源对象,代表一个资源
     public MyClass()
     {
          handle=OpenHandle(); //获取资源
     }
     public void Dispose()
     {
          CloseHandle(handle)  //释放资源
          GC.SuppressFinalize(this);  //告诉系统就当我没有写析构器,避免托大代龄
         }
     ~MyClass()  //析构器
     {
          CloseHandle(handle)  //释放资源
     }
     public void Process()
     {
          //...........
         }
    }
    class Test
    {
         public static void Main()
         {
              MyClass obj=null;
              try
              {
                   obj=new MyClass();
                   obj.Process();
              }
              catch(Exception e)
              {
                throw e;
              }
              finally
              {
                  if (obj!=null)
                  {
                   obj.Dispose();
               }
              }              
              //............
              
//............         
              
//GC启动
         }
    }


这样就确保了Dispose()方法会被执行,这种确保资源被释放的处理方法可以使用using语句来处理,using语句块可以确保对象的Dispose()方法被执行,不管是否出现异常,其实using语句的实现就是通过上面的异常处理方式实现的,因此前提是对象的类实现了IDisposable接口,内部具有Dispose()方法。上面的代码可以改为:using System;
class MyClass : IDisposable
{  
     int x;         //纯内存对象,垃圾收集器可完全回收
     int y;     
     IntPtr handle  //资源对象,代表一个资源
     public MyClass()
     {
          handle=OpenHandle(); //获取资源
     }
     public void Dispose()
     {
          CloseHandle(handle)  //释放资源
          GC.SuppressFinalize(this);  //告诉系统就当我没有写析构器,避免托大代龄
         }
     ~MyClass()  //析构器
     {
          CloseHandle(handle)  //释放资源
     }
     public void Process()
     {
          //...........
         }
    }
    class Test
    {
         public static void Main()
         {
              using(MyClass obj=new MyClass())
              {
                   obj.Process();
              }         
              //............
              
//............         
              
//GC启动
         }
    }

这里补充一些:析构器里面一般只用于释放资源,最好不要去做其他事情,因为在有些情况下,析构器根本就不会被调用。





本文链接



您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-4-26 03:43 , Processed in 0.016272 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表