托管资源主要是指托管堆上分配的内存资源, 其由.NET运行时在适当的时候调用垃圾回收器进行回收。非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源如文件、窗口、网络连接、数据库连接、画刷、图标等。
托管资源
托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,由.NET运行时在适当的时候调用垃圾回收器进行回收。例如程序中分配的对象,作用域内的变量等。但如果我们需要立即进行垃圾回收,则可以使用GC.Collect()
来触发。有关内存管理和垃圾回收,我们将在下一篇文章讲解。
GC.Collect
一般情况下,我们并不需要调用GC.Collect()
来触发垃圾回收。因为,让垃圾回收期自动去确定垃圾回收的时机往往会拥有更好的性能(在垃圾回收器执行之前,它会挂起当前正在运行的所有线程,以方便它检测各种对象之间的关联关系,从而分析出需要回收的垃圾)。
然而,自动的垃圾回收时机有时候无法满足我们的要求:比如大数据量或大图的载入。
不过,事物具有两面性。鉴于GC
执行之前会挂起线程,故我们不应该频繁地调用GC.Collect()
。否则可能会造成性能下降。
另外,关于GC.Collect()
的使用,不在此赘述了。提醒一点的是,此方法的重载GC.Collect(int generation)
,参数表示此次需要强制回收到第几代的对象。
托管执行过程
包括以下几个步骤:
- 选择编译器
- 将代码编译为
MSIL
- 将
MSIL
编译为本机代码 - 运行代码
选择编译器
即选择对应语言的编译器,来进行语言级别的处理,如语法检查;
将代码编译为 MSIL
编译器将源代码转换为一组独立于CPU且可转换为本地代码的指令,即MSIL
。它包括有关加载、存储、初始化和调用对象方法的指令,以及有关算术和逻辑运算、控制流、直接内存访问、异常处理和其他操作的指令。当编译器生成 MSIL
时,它还生成元数据,其描述代码中的类型,包括每种类型的定义、每种类型的成员的签名、代码引用的成员以及运行时在执行时间使用的其他数据。
正是文件中元数据的存在以及 MSIL
使代码的自描述性,使得我们不需要类型库或接口定义语言 (IDL
)。因为运行时在执行期间会根据需要从文件中查找并提取元数据。
将 MSIL
编译为本机代码
.NET framework提供了两种转换方式:JIT(实时编译器)和NGen.exe(本机映像生成器)
- JIT(实时编译器): 在加载和执行程序集的内容时,
JIT
编译器将在应用程序运行时按需将MSIL
转换为本机代码 - NGen.exe:因为JIT是在运行时将MSIL转换为本机代码,而这会导致性能的下降;并且,JIT生成的代码会绑定到触发编译的进程上,从而导致这些代码无法在多个进程之间共享。 因此,NGen.exe应运而生。
NGen.exe(本机映像生成器)有着JIT无法替代的优势:
- 允许生成的代码跨应用程序的多个调用或跨共享一组程序集的多个进程进行共享
- 它一次编译整个程序集,而不是一次编译一种方法:虽然首次编译较慢,但运行时却会快一些,有助于提升用户体验
- 提供代码验证机制:检查 MSIL 和元数据,以找出代码是否为类型安全,其有助于将对象隔离开来,保护这些对象免受有意或无意的损坏
运行代码
CLR为托管代码的执行,提供基础服务:在执行期间,托管代码接收服务,如垃圾收集、安全性、与非托管代码的互操作性、跨语言调试支持以及增强的部署和版本控制支持。
非托管资源
非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源如文件,窗口,网络连接,数据库连接,画刷,图标等。这类资源,垃圾回收器在清理的时候会调用Object.Finalize()
方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。
对于 Finalize
方法,需要注意:
- 无法显式的重写
Finalize
方法,只能通过析构函数语法形式来实现 struct
中不允许定义析构函数,只有class
中才可以,并且只能有一个Finalize
方法不能被继承或重载- 执行垃圾回收之前系统会自动执行
Finalize
操作 Finalize
方法会极大地损伤性能
为了更方便的释放非托管资源,微软为非托管资源的回收专门定义了一个接口:IDisposable
,接口中只包含一个Dispose()
方法。任何包含非托管资源的类,都应该继承此接口,以确保非托管资源的正确释放。
在一个包含非托管资源的类中,资源释放的标准做法是:
- 继承
IDisposable
接口 - 实现
Dispose()
方法,在其中释放非托管资源,并将对象本身从垃圾回收器中移除(为了告诉垃圾回收器,此对象的资源我们已经自己释放了,它就没必要再对此对象进行回收了); - 实现类析构函数,在其中释放非托管资源。
示例代码如下:
public class UnmanagedObject : IDisposable {
private bool hasDisposed = false;
~UnmanagedObject() {
this.ReleaseResources(false);
}
public void Dispose() {
this.ReleaseResources(true);
/// 禁止GC调用此对象的 Finalizer,以提高性能
GC.SuppressFinalize(this);
}
/// <summary>
/// 释放资源
/// </summary>
/// <param name="explicitInvoke">
/// True: 说明此方法是被程序在其他地方显示调用的,因此需要我们释放非托管资源(此对象引用的和成员引用的);
/// False: 说明此方法是被析构函数调用的,只需要释放此对象的非托管资源。因为其他的(此对象成员引用的)非托管资源,GC会自己处理。
/// </param>
protected virtual void ReleaseResources(bool explicitInvoke) {
if (!this.hasDisposed) {
if (explicitInvoke) {
// 此处释放此对象中实现了 IDisposable 的成员所引用的资源
// 因为我们禁止了GC调用此对象的 Finalizer,所以需要我们手动的释放成员的资源;否则,成员引用的托管资源将无法释放。
}
// 此处释放非托管资源
// 放在此处,可保证无论是显式调用 Dispose 方法,还是让析构函数的处理,都可以保证非托管资源最终被释放
this.hasDisposed = true;
}
}
}
建议使用方式如下:
using (UnmanagedObject unmanagedObject = new UnmanagedObject()) {
// 其他代码
}
优势如下:
- 显示调用
Dispose()
方法,可以及时的释放资源; - 就算没有显示调用
Dispose()
方法,垃圾回收器也可以通过析构函数来释放非托管资源,垃圾回收器本身就具有回收托管资源的功能,从而可保证资源的正常释放。只不过,由垃圾回收器回收会导致非托管资源未及时释放而造成内存的浪费(最终还是会被释放)。
但我们还是应该尽量少用析构函数释放资源,因为:
- 没有析构函数的对象在垃圾回收器第一次处理中便从内存删除;但有析构函数的对象,需要两次,第一次调用析构函数,第二次将对象从内存删除。
- 在析构函数中包含大量释放资源的代码,会降低垃圾回收器的工作效率,影响性能。
因此,对于包含非托管资源的对象,最好及时的调用Dispose()
方法来回收资源(使用using
更方便),而不是依赖垃圾回收器。