.NET垃圾回收 – 非托管资源

时间:2021-03-13 00:04:47

前面一篇文章介绍了垃圾回收的基本工作原理,垃圾回收器并不是可以管理内存中的所有资源。对于所有的托管资源都将有.NET垃圾回收机制来释放,但是,对于一些非托管资源,我们就需要自己编写代码来清理这类资源了。

其实在C#开发中,大部分资源都可以通过.NET垃圾回收机制进行回收,只用当我们使用非托管资源(原始的操作系统文件句柄,原始的非托管数据库连接,非托管内存等等)的时候,我们才需要实现自己的资源清理代码。

.NET提供了两种释放非托管资源的方式,类型自己的Finalize方法和IDisposable接口的Dispose方法。

下面就来看看这两个跟垃圾回收相关的方法。

Finalize方法

在.NET的基类System.Object中,定义了名为Finalize()的虚方法,这个方法默认什么都不做。

我们可以为自定义的类型重写Finalize方法,在该方法中加入必要的非托管资源清理逻辑。当要从内存中删除这个类型的对象时,垃圾回收器会调用对象的Finalize方法。所以,无论.NET进行一次自发的垃圾回收,还是我们通过GC.Collect()进行强制垃圾回收,Finalize方法总是会被调用。另外,当承载应用程序的AppDomain从内存中移除时,同样会调用Finalize方法。

重写Finalize方法

假设我们现在有一个使用非托管资源的类型,那么我们就需要重写Finalize方法来进行非托管资源的清理,但是当通过下面的方式重写Finalize方法的时候,我们会得到一个编译错误。

class MyResourceWrapper
{
protected override void Finalize()
{ }
}

.NET垃圾回收 – 非托管资源

其实,当我们想要重写Finalize方法时,C#为我们提供了(类似C++)析构函数语法(C#终结器)来重写该方法。C#终结器和构造函数语法类似,方法名称都和类型名称一样;不同的是,终结器具有~前缀,并且不能使用访问修饰符,不接受参数,也不能重载,所以一个类只能有一个终结器。

class MyResourceWrapper
{
~MyResourceWrapper()
{
Console.WriteLine("release unmanaged resources");
Console.Beep();
}
}

之所以C#只支持这种方式进行Finalize方法的重写,是因为C#编译器会为Finalize方法隐式地加入一些必需的基础代码。下面就是我们通过ILSpy查看到了IL代码,Finalize方法作用域内的代码被放在了一个try块中,然后不管在try块中是否遇到异常,finally块保证了Finalize方法总是能够被执行

.method family hidebysig virtual
instance void Finalize () cil managed
{
// Method begins at RVA 0x2050
// Code size 31 (0x1f)
.maxstack .try
{
IL_0000: nop
IL_0001: ldstr "release unmanaged resources"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: call void [mscorlib]System.Console::Beep()
IL_0011: nop
IL_0012: nop
IL_0013: leave.s IL_001d
} // end .try
finally
{
IL_0015: ldarg.0
IL_0016: call instance void [mscorlib]System.Object::Finalize()
IL_001b: nop
IL_001c: endfinally
} // end handler IL_001d: nop
IL_001e: ret
} // end of method MyResourceWrapper::Finalize

当我们执行下面代码时,我们就可以听到系统蜂鸣声,像我们前面介绍的一样AppDomain被移除内存,类型终结器将被调用。

static void Main(string[] args)
{
MyResourceWrapper mr = new MyResourceWrapper();
}

Finalize的工作机制

Finalize的工作机制还是比较复杂的,这里只是简单的介绍,更多的原理大家可以自己网上查查。

当在托管堆上分配对象空间时,运行库会自动确定该对象是否提供一个自定义的Finalize方法。如果是这样,对象被标记为可终结的,同时一个指向这个对象的指针被保存在名为终结队列的内部队列中。终结队列是一个由垃圾回收器维护的表,它指向每一个在从堆上删除之前必须终结的对象。

当垃圾回收器确定到了从内存中释放一个对象的时间时,它检查终结队列上的每一个项,并将对象从堆上复制到另一个称作终结可达表(finalization reachable table的托管结构上。此时,下一个垃圾回收时将产生另外一个线程,为每一个在可达表中的对象调用Finalize方法。因此,为了真正终结一个对象,至少要进行两次垃圾回收。

从上面可以看到,Finalize方法的调用是相当消耗资源的。Finalize方法的作用是保证.NET对象能够在垃圾回收时清理非托管资源,如果创建了一个不使用非托管资源的类型,实现终结器是没有任何作用的。所以说,如果没有特殊的需求应该避免重写Finalize方法。

IDisposable接口

当垃圾回收生效时,可以利用终结器来释放非托管资源。然而,很多非托管资源都非常宝贵(如数据库和文件句柄),所以它们应该尽可能快的被清除,而不能依靠垃圾回收的发生。除了重写Finalize之外,类还可以实现IDisposable接口,然后在代码中主动调用Dispose方法来释放资源。

看一个例子:

class MyResourceWrapper:IDisposable
{
public void Dispose()
{
Console.WriteLine("release resources with Dispose");
Console.Beep();
}
} class Program
{
static void Main(string[] args)
{
MyResourceWrapper mr = new MyResourceWrapper();
mr.Dispose();
}
}

同样,当我们显示的调用Dispose方法的时候,可以听到系统的蜂鸣声。

注意,通过Dispose进行资源的释放也是有潜在的风险的,因为Dispose方法需要被程序员显示的调用,如果代码中漏掉了Dispose的调用或者在Dispose调用之前产生了异常从而没有指定Dispose,那么有些资源可能就一直留在内存中了。

所以我们应该使用下面的方式保证Dispose方法可以被调用到:

static void Main(string[] args)
{
MyResourceWrapper mr = new MyResourceWrapper();
try
{
//do something wiht mr object
}
finally
{
mr.Dispose();
}
}

但是,每次编写Dispose的代码都使用try块会觉得很麻烦,还好C#中,我们可以重用using关键字来简化Dispose的调用。

重用using关键字

在C#中,using语句提供了一个高效的调用对象Dispose方法的方式。对于任何IDispose接口的类型,都可以使用using语句,而对于那些没有实现IDisposable接口的类型,使用using语句会导致一个编译错误。

static void Main(string[] args)
{
using (MyResourceWrapper mr = new MyResourceWrapper())
{
//do something with mr object
}
}

在using语句块结束的时候,mr实例的Dispose方法将会被自动调用。using语句不仅免除了程序员输入Dispose调用的代码,它还保证Dispose方法被调用,无论using语句块顺利执行结束,还是抛出一个异常。事实上,C#编译器为using语句自动添加了try/finally块。我们可以看看using的IL代码:

.try
{
IL_0007: nop
IL_0008: nop
IL_0009: leave.s IL_001b
} // end .try
finally
{
IL_000b: ldloc.0
IL_000c: ldnull
IL_000d: ceq
IL_000f: stloc.1
IL_0010: ldloc.1
IL_0011: brtrue.s IL_001a IL_0013: ldloc.0
IL_0014: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0019: nop IL_001a: endfinally
} // end handler

Dispose和Finalize的结合

从前面的介绍了解到,Finalize可以通过垃圾回收进行自动的调用,而Dispose需要被代码显示的调用,所以,为了保险起见,对于一些非托管资源,还是有必要实现终结器的。也就是说,如果我们忘记了显示的调用Dispose,那么垃圾回收也会调用Finalize,从而保证非托管资源的回收。

其实,MSDN上给我们提供了一种很好的模式来实现IDisposable接口来结合Dispose和Finalize,例如下面的代码:

class MyResourceWrapper:IDisposable
{
private bool IsDisposed=false; public void Dispose()
{
Dispose(true);
//tell GC not invoke Finalize method
GC.SuppressFinalize(this);
} protected void Dispose(bool Disposing)
{
if(!IsDisposed)
{
if(Disposing)
{
//clear managed resources
}
//clear unmanaged resources
}
IsDisposed=true;
} ~MyResourceWrapper()
{
Dispose(false);
}
}

在这个模式中,void Dispose(bool Disposing)函数通过一个Disposing参数来区别当前是否是被Dispose()调用。如果是被Dispose()调用,那么需要同时释放托管和非托管的资源。如果是被终结器调用了,那么只需要释放非托管的资源即可。Dispose()函数是被其它代码显式调用并要求释放资源的,而Finalize是被GC调用的。

另外,由于在Dispose()中已经释放了托管和非托管的资源,因此在对象被GC回收时再次调用Finalize是没有必要的,所以在Dispose()中调用GC.SuppressFinalize(this)避免重复调用Finalize。同样,因为IsDisposed变量的存在,资源只会被释放一次,多余的调用会被忽略。

所以这个模式的优点可以总结为:

  1. 如果没有显示的调用Dispose(),未释放托管和非托管资源,那么在垃圾回收时,还会执行Finalize(),释放非托管资源,同时GC会释放托管资源
  2. 如果调用了Dispose(),就能及时释放了托管和非托管资源,那么该对象被垃圾回收时,就不会执行Finalize(),提高了非托管资源的使用效率并提升了系统性能

总结

本文介绍了.NET垃圾回收中两个相关的方法:Dispose和Finalize。Finalize的目的是用于释放非托管的资源,而Dispose是用于释放所有资源,包括托管的和非托管的。

Dispose需要在代码中进行显示的调用,而Finalize则是由垃圾回收自动调用,为了更有效的结合Dispose和Finalize,文中还介绍了MSDN中给出的实现IDisposable接口的一个模式。

.NET垃圾回收 – 非托管资源的更多相关文章

  1. CLR回收非托管资源

    一.非托管资源 在<垃圾回收算法之引用计数算法>.<垃圾回收算法之引用跟踪算法>和<垃圾回收算法之引用跟踪算法>这3篇文章中,我们介绍了垃圾回收的一些基本概念和原理 ...

  2. C&num; 托管资源与非托管资源

    在.net 编程环境中,系统的资源分为托管资源和非托管资源. 托管资源: Net平台中,CLR为程序员提供了一种很好的内存管理机制,使得程序员在编写代码时不要显式的去释放自己使用的内存资源(这些在先前 ...

  3. C&num; 托管资源和非托管资源

    托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源.托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收. 非托管资源指的是.NET不知道如何回 ...

  4. 5&period;C&num;释放非托管资源1

    释放非托管资源 在介绍释放非托管资源的时候,我觉得有必要先来认识一下啥叫非托管资源,既然有非托管资源,肯定有托管资源. 托管资源指的是.net可以自棕进行回收的资源,主要是指托管堆上分配的内存资源.托 ...

  5. C&num;编程(七十四)----------释放非托管资源

    释放非托管资源 在介绍释放非托管资源的时候,我觉得有必要先来认识一下啥叫非托管资源,既然有非托管资源,肯定有托管资源. 托管资源指的是.net可以自棕进行回收的资源,主要是指托管堆上分配的内存资源.托 ...

  6. C&num; 托管非托管资源释放

    1.C#几乎所有对象都为托管对象,不同点是有的对象封装了非托管资源. 2.C#大部分对象在进行垃圾回收时都可以回收,包括非托管资源,因为非托管资源都已经通过C#类进行了封装,会将非托管资源的释放放在析 ...

  7. &lbrack;转&rsqb;在C&num;中使用托管资源和非托管资源的区别,以及怎样手动释放非托管资源&colon;

    托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源.托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收. 非托管资源指的是.NET不知道如何回 ...

  8. 转&sol; C&num; 托管资源和非托管资源

    原文 对于这两个一直就是模模糊糊的,半知零解 托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源.托管资源的回收工作是不需要人工干预的,由.NET运行库在合适时调用垃圾回收器 ...

  9. 阶段复习-&period;NET下托管资源与非托管资源的小记

    托管资源由由程序员负责分配,在系统的二级缓存中,GC自动回收释放:而非托管资源也是由程序员负责分配,资源的释放回收也是由程序员负责,使用Dispose或者析构函数对资源进行回收,常见的非托管资源是包装 ...

随机推荐

  1. &lbrack;ucgui&rsqb; 对话框6——触屏位置简单例子

    >_<:直接调用函数获得触屏位置: xPhys = GUI_TOUCH_GetxPhys(); /* Get the A/D mesurement result in x */ yPhys ...

  2. ubuntu16&period;04&plus;opencv3&period;1配置

    #安装必要的依赖sudo apt-get install build-essential cmakesudo apt-get install cmake Git libgtk2.0-dev pkg-c ...

  3. Android监听键盘显示和隐藏

    问题概况:横板cocos2dx游戏,点击输入框弹出键盘时,界面要求跟随网上平易,不能挡住输入框.这种问题只出现在非全屏键盘到情况下. 方案1:mainActivity重写onconfiguration ...

  4. 从一个简单的Java单例示例谈谈并发

    一个简单的单例示例 单例模式可能是大家经常接触和使用的一个设计模式,你可能会这么写 public class UnsafeLazyInitiallization { private static Un ...

  5. 读书笔记—CLR via C&num;章节3

    这本书这几年零零散散读过两三遍了,作为经典书籍,应该重复读反复读,既然我现在开始写博了,我也准备把以前觉得经典的好书重读细读一遍,并且将笔记整理到博客中,好记性不如烂笔头,同时也在写的过程中也可以加深 ...

  6. GPU编程--宏观理解篇(1)

    GPU编程与CPU编程最大的不同可以概括为以下两点: "The same program is executed on many data elements in parallel&quot ...

  7. DLL基础

    Visual C++在创建DLL导出函数时,可能会对原始的函数名做修改.例如: int WINAPI Add(int nLeft, int nRight) 导出后的函数名称是_Add@8. 下面两种方 ...

  8. mac SecureCRT设置

    参考: http://www.2cto.com/os/201407/320292.html SecureCRT 设置 1)每次登陆都要输入密码: Global Option -> General ...

  9. 流API--流的映射

    很多时候,将一个流的元素映射到另外一个流很有帮助.映射操作最具代表的就是map()方法.实际编码中,我们会经常用到,所以这里专门整理一篇博客. 考虑如下情景,对于一个包含了姓名,电话,年龄等属性构成的 ...

  10. ElasticSearch - Shard数调优(ElasticSearch性能)

    序言 资料 https://blog.csdn.net/waneyongfu/article/details/78215972