c#的内存管理(托管及未托管对象管理)

c#中的对象分为值类型和引用类型,二者最大的区别在于数据的存储方式和存储位置.WINDOWS操作系统使用虚拟寻址系统来管理程序运行时产生的数据存放.简单的说,该系统管理着一个内存区域,在该区域中划拨出一部分出来专门存放值类型变量,称为堆栈,堆栈采用先进后出的原则,将值类型变量从区域的最高地址位开始向低位地址存储,先进后出,后进先出的管理方式保证了值类型变量在出了作用域后能即使的清除占用的内存区域,由于堆栈速度快,所保存的数据一般不太大,这部分一般不需要用户专门操作. 值类型保存在堆栈汇总, 堆栈有非常高的性能,但对于所有的变量来说还是不太灵活。通常我们希望使用一个方法分配内存,来存储一些数据,并在方法退出后的很长一段时间内数据仍是可以使用的。只要是用new运算符来请求存储空间,就存在这种可能性——例如所有的引用类型。此时就要使用托管堆。它在垃圾收集器的控制下工作,托管堆(或简称为堆)是系统管理的大内存区域中的另一个内存区域。要了解堆的工作原理和如何为引用数据类型分配内存,看看下面的代码: 

Customer arabel  =  new Customer();
这行代码完成了以下操作:首先,分配堆上的内存,以存储Customer实例(一个真正的实例,不只是一个地址)。然后把变量arabel的值设置为分配给新Customer对象的内存地址(它还调用合适的Customer()构造函数初始化类实例中的字段,但我们不必担心这部分)。
Customer实例没有放在堆栈中,而是放在内存的堆中。如果我们这样操作:
Customer newaddress =  arabel ;
这时候,newaddress也会保存在堆栈中,其值和arabel  相同,都是存储Customer实例的堆地址.
     知道了这些,我们会发现这样一个问题,如果堆栈中arabel 和newaddress两个变量过期销毁,那堆中保存的Customer对象会怎样?实际上它仍保留在堆中,一直到程序停止,或垃圾收集器删除它为止. C#的垃圾收集器如果没有显示调用,会定时运行并检查内存,删除没有任何变量引用的数据.看起来似乎不错,但是想想,垃圾回收器并不是时时检查,它是定时运行,而在这段时间内如果产生大量的过期数据驻留在内存中..... 那么或许我们可以通过调用System.GC.Collect(),强迫垃圾收集器在代码的某个地方运行,System.GC是一个表示垃圾收集器的.NET基类, Collect()方法则调用垃圾收集器。但是,这种方式适用的场合很少,(难道销毁一个对象就让垃圾回收检查一便内存吗?)例如,代码中有大量的对象刚刚停止引用,就适合调用垃圾收集器。况且垃圾收集器的逻辑不能保证在一次垃圾收集过程中,从堆中删除所有过期数据,对于不受垃圾回收器管理的未托管对象(例如文件句柄、网络连接和数据库连接),它是无能为力的。那该怎么做呢?
  这时需要制定专门的规则,确保未托管的资源在回收类的一个实例时释放。
在定义一个类时,可以使用两种机制来自动释放未托管的资源。这些机制常常放在一起实现,因为每个机制都为问题提供了略为不同的解决方法。这两个机制是:
●        声明一个析构函数,作为类的一个成员
●        在类中实现System.IDisposable接口
下面依次讨论这两个机制,然后介绍如何同时实现它们,以获得最佳的效果。
析构函数
前面介绍了构造函数可以指定必须在创建类的实例时进行的某些操作,在垃圾收集器删除对象时,也可以调用析构函数。由于执行这个操作,所以析构函数初看起来似乎是放置释放未托管资源、执行一般清理操作的代码的最佳地方。但是,事情并不是如此简单。由于垃圾回首器的运行规则决定了,不能在析构函数中放置需要在某一时刻运行的代码,如果对象占用了宝贵而重要的资源,应尽可能快地释放这些资源,此时就不能等待垃圾收集器来释放了.
IDisposable接口
一个推荐替代析构函数的方式是使用System.IDisposable接口。IDisposable接口定义了一个模式(具有语言级的支持),为释放未托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾函数器相关的问题。IDisposable接口声明了一个方法Dispose(),它不带参数,返回void,Myclass的方法Dispose()的执行代码如下:
class Myclass : IDisposable
{
   public void Dispose() 
   {
      // implementation
   }
}
Dispose()的执行代码显式释放由对象直接使用的所有未托管资源,并在所有实现IDisposable接口的封装对象上调用Dispose()。这样,Dispose()方法在释放未托管资源时提供了精确的控制。
假定有一个类ResourceGobbler,它使用某些外部资源,且执行IDisposable接口。如果要实例化这个类的实例,使用它,然后释放它,就可以使用下面的代码: 
ResourceGobbler theInstance = new ResourceGobbler();

   // 这里是theInstance 对象的使用过程
  
theInstance.Dispose();
如果在处理过程中出现异常,这段代码就没有释放theInstance使用的资源,所以应使用try块,编写下面的代码:
ResourceGobbler theInstance = null;
try
{
   theInstance = new ResourceGobbler();
//  这里是theInstance 对象的使用过程
}
finally  
{
  if (theInstance != null) theInstance.Dispose();
}
即使在处理过程中出现了异常,这个版本也可以确保总是在theInstance上调用Dispose(),总是释放由theInstance使用的资源。但是,如果总是要重复这样的结构,代码就很容易被混淆。C#提供了一种语法,可以确保在引用超出作用域时,在对象上自动调用Dispose()(但不是Close())。该语法使用了using关键字来完成这一工作—— 但目前,在完全不同的环境下,它与命名空间没有关系。下面的代码生成与try块相对应的IL代码:
using (ResourceGobbler theInstance = new ResourceGobbler())
{
   //  这里是theInstance 对象的使用过程
}
using语句的后面是一对圆括号,其中是引用变量的声明和实例化,该语句使变量放在随附的复合语句中。另外,在变量超出作用域时,即使出现异常,也会自动调用其Dispose()方法。如果已经使用try块来捕获其他异常,就会比较清晰,如果避免使用using语句,仅在已有的try块的finally子句中调用Dispose(),还可以避免进行额外的缩进。
注意:
对于某些类来说,使用Close()要比Dispose()更富有逻辑性,例如,在处理文件或数据库连接时,就是这样。在这些情况下,常常实现IDisposable接口,再执行一个独立的Close()方法,来调用Dispose()。这种方法在类的使用上比较清晰,还支持C#提供的using语句。

前面的章节讨论了类所使用的释放未托管资源的两种方式:
●        利用运行库强制执行的析构函数,但析构函数的执行是不确定的,而且,由于垃圾收集器的工作方式,它会给运行库增加不可接受的系统开销。
●        IDisposable接口提供了一种机制,允许类的用户控制释放资源的时间,但需要确保执行Dispose()。
一般情况下,最好的方法是执行这两种机制,获得这两种机制的优点,克服其缺点。假定大多数程序员都能正确调用Dispose(),实现IDisposable接口,同时把析构函数作为一种安全的机制,以防没有调用Dispose()。下面是一个双重实现的例子:
public class ResourceHolder : IDisposable
{
      private bool isDispose = false;
      
      // 显示调用的Dispose方法
  public void Dispose() 
      {
           Dispose(true);
          GC.SuppressFinalize(this); 
       }

       // 实际的清除方法
  protected virtual void Dispose(bool disposing) 
      {
            if (!isDisposed)
           {
              if (disposing) 
           { 
                      // 这里执行清除托管对象的操作.
                  }
                  // 这里执行清除非托管对象的操作
            }
    
         isDisposed=true;
      }

      // 析构函数 
      ~ResourceHolder()
      {
            Dispose (false);
      }
}
可以看出,Dispose()有第二个protected重载方法,它带一个bool参数,这是真正完成清理工作的方法。Dispose(bool)由析构函数和IDisposable.Dispose()调用。这个方式的重点是确保所有的清理代码都放在一个地方。
传递给Dispose(bool)的参数表示Dispose(bool)是由析构函数调用,还是由IDisposable.Dispose()调用——Dispose(bool)不应从代码的其他地方调用,其原因是:
●        如果客户调用IDisposable.Dispose(),该客户就指定应清理所有与该对象相关的资源,包括托管和非托管的资源。
●        如果调用了析构函数,在原则上,所有的资源仍需要清理。但是在这种情况下,析构函数必须由垃圾收集器调用,而且不应访问其他托管的对象,因为我们不再能确定它们的状态了。在这种情况下,最好清理已知的未托管资源,希望引用的托管对象还有析构函数,执行自己的清理过程。
isDispose成员变量表示对象是否已被删除,并允许确保不多次删除成员变量。这个简单的方法不是线程安全的,需要调用者确保在同一时刻只有一个线程调用方法。要求客户进行同步是一个合理的假定,在整个.NET类库中反复使用了这个假定(例如在集合类中)。最后,IDisposable.Dispose()包含一个对System.GC. SuppressFinalize()方法的调用。SuppressFinalize()方法则告诉垃圾收集器有一个类不再需要调用其析构函数了。因为Dispose()已经完成了所有需要的清理工作,所以析构函数不需要做任何工作。调用SuppressFinalize()就意味着垃圾收集器认为这个对象根本没有析构函数.

  正确理解以上内容,可以大大优化系统性能,及时释放不需要的数据,不能仅靠C#提供的自动回收机制,也需要程序员使用更灵活的办法!二者合一既能让程序运行飞快,也让系统更加稳定!

(参考文档:清华大学出版社出版的Wrox红皮书《C#高级编程(第3版)》)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C#中创建的对象会由垃圾回收器自动释放,无需手动释放。垃圾回收器会自动检测对象的引用计数,当对象的引用计数为0时,垃圾回收器会自动回收对象内存空间。这种自动释放的机制称为托管内存的自动回收。 在C#中,当对象不再被引用时,垃圾回收器会在适当的时机自动回收对象内存空间。这种自动回收的机制可以避免内存泄漏和手动释放内存的麻烦。 在C#中,可以通过手动释放对象内存空间的方式是实现IDisposable接口。IDisposable接口定义了一个Dispose方法,用于手动释放对象的资源。在Dispose方法中,可以释放托管资源和非托管资源。 下面是一个示例代码,演示了如何手动释放托管代码中创建的对象: ```C# public class MyClass : IDisposable { // 托管资源 private List<int> myList = new List<int>(); // 非托管资源 private IntPtr myPtr = IntPtr.Zero; // 实现IDisposable接口的Dispose方法 public void Dispose() { // 释放托管资源 myList.Clear(); // 释放非托管资源 if (myPtr != IntPtr.Zero) { // 释放myPtr指向的内存空间 // ... myPtr = IntPtr.Zero; } // 告诉垃圾回收器不再调用析构函数 GC.SuppressFinalize(this); } // 析构函数 ~MyClass() { // 在析构函数中调用Dispose方法 Dispose(); } } // 使用MyClass class Program { static void Main(string[] args) { using (MyClass myObj = new MyClass()) { // 使用myObj } // myObj超出using作用域后,Dispose方法会自动调用,释放对象内存空间 } } ``` 在上面的示例中,使用using语句创建了一个MyClass对象myObj,使用完myObj后,myObj超出using作用域后,Dispose方法会自动调用,释放对象内存空间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值