垃圾回收
垃圾回收是指由回收不再被引用的对象所占用的内存。
垃圾回收器只回收内存,不处理其他资源,比如数据库连接、句柄(文件、窗口等)、网络端口以及硬件设备(比如串口)。
.NET垃圾回收原理
.NET 的垃圾回收器采用mark-and-compact算法。一次垃圾回收过程开始时,垃圾回收器从根引用(静态变量、CPU寄存器、局部变量或参数实例)查找所有可达对象,然后移动可达对象紧挨着放在一起,最后释放所有不可达对象所占用的内存。
在一次垃圾回收过程中,并不会清除所有未引用对象。根据对象的生存期统计,相比于长期存在的对象,最近创建的对象更可能需要被垃圾回收。因此,.NET垃圾回收器支持“代”(generation)的概念。堆被分为3代,新创建的对象被放在第0代堆上,这部分会以较高的频率执行垃圾回收,在一次垃圾回收后,“存活”下来的对象会被移动到下一代,最高移动到第2代。越高的代执行垃圾回收的频率越低。
可以调用System.GC.Collect()方法强制启动一个垃圾回收过程,但一般不需要使用。如果调用无参数System.GC.Collect(),则会对所有”代“执行垃圾回收。
资源释放
垃圾回收能自动释放托管对象的内存,但并不会释放非托管资源(例如数据库连接、文件句柄、网络连接等)。
当托管类在封装对非托管资源的直接或间接引用时,需要用下文的方法确保非托管资源的释放。
析构函数
C#的析构函数的语法与C++类似,带有~前缀,之后与类名相同,没有返回类型,不带参数,没有访问修饰符。示例:- class MyClass
- {
- ~MyClass()
- {
- // Finalizer implementation
- }
- }
复制代码 C#编译器在编译析构函数时,会隐式地把析构函数的代码编译为重写Finalize()方法的等价代码。
下面代码是编译器编译~MyClass()生成的IL:- protected override void Finalize()
- {
- try
- {
- // Finalizer implementation
- }
- finally
- {
- base.Finalize();
- }
- }
复制代码C#的析构函数也被称为终结器(Finalizer)。
析构函数不能在代码中显式调用,而是在垃圾回收前被自动调用。(如果进程异常终止,终结器将不会运行,例如计算机断电或进程被强制终止。)
没有析构函数的对象在垃圾回收时将被直接释放内存,而具有析构函数的对象在垃圾回收时,先被添加到f-reachable队列,然后在一个独立的线程中执行析构函数,执行完成后从f-reachable队列中移除,然后在下一次垃圾回收释放内存。这将有以下影响:
- 不能确定析构函数什么时候会被执行。
- 将延迟对象从内存中删除的时间,因为f-reachable队列引用了对象,在从f-reachable队列移除前,垃圾回收器不能释放此对象的内存。因此需要两次垃圾回收才能释放内存。
- 析构函数中的代码是线程不安全的,需谨慎访问其他托管对象。
- 如果析构函数引发了未处理异常,会难以诊断,所以要避免在析构函数中引发异常。
IDisposable接口
在C#中,推荐使用System.IDisposable接口代替析构函数释放非托管资源。IDisposable接口定义了一种模式(具有语言级的支持),该模式为释放非托管的资源提供了确定性的机制,并避免析构函数与垃圾回收相关的问题。
当托管类封装了对非托管资源的间接引用时(也就是已变为托管资源),即托管类封装了具有IDisposable接口的对象,只需如下简单实现IDisposable接口:- class MyClass : IDisposable
- {
- private MyManagedResource _myManagedResource;
- public void Dispose()
- {
- // implementation
- _myManagedResource?.Dispose();
- }
- }
复制代码 Dispose()方法中调用实现了IDisposable接口的封装对象的Dispose()方法。
当托管类封装了对非托管资源的直接引用时,需确保调用Dispose()方法,通过实现析构函数做为一种安全机制,以便在代码中没有调用Dispose()方法时,由析构函数调用Dispose()方法,示例:- class MyClass : IDisposable
- {
- private bool _disposedValue;
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposedValue)
- {
- if (disposing)
- {
- // TODO: 释放托管状态(托管对象)
- }
- // TODO: 释放未托管的资源(未托管的对象)并重写终结器
- // TODO: 将大型字段设置为 null
- _disposedValue = true;
- }
- }
- // TODO: 仅当“Dispose(bool disposing)”拥有用于释放未托管资源的代码时才定义终结器
- ~MyClass()
- {
- // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
- Dispose(disposing: false);
- }
- public void Dispose()
- {
- // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
- }
复制代码 上述代码中:
- Dispose(bool disposing)方法中真正完成清理工作,其参数用于区分是由Dispose()方法调用,还是由析构函数调用,其原因是:
- 如果使用者调用Dispose(),应清理所有与该对象相关的资源,包括托管和非托管的资源。
- 如果调用了析构函数,原则上所有的资源仍需要清理,但是析构函数由垃圾回收器调用,不应访问其他托管的对象,因为析构函数被调用的顺序是不确定的,其他托管对象的析构函数可能已经先被调用。这种情况下,最好只清理非托管资源,引用的任何托管对象由它们自己的析构函数执行清理过程。
- _disposedValue成员变量表示对象是否已被清理,确保不试图多次清理资源。它也可以用于在执行实例方法之前检查对象是否已清理。
- Dispose()方法包含对System.GC.SuppressFinalize()方法的调用。SuppressFinalize()方法告诉垃圾回收器不需要调用这个对象的析构函数了,即等同于这个对象没有析构函数,因为Dispose()方法已经完成了所需的清理工作,所以析构函数不需要做任何工作。这样就避免了析构函数导致的对象延迟释放。
使用using语句调用Dispose()方法
使用Dispose()方法的简单用法如下:- var myClass = new MyClass();
- // do your processing
- myClass.Dispose();
复制代码 如果中间出现了异常代码将不会运行到Dispose()方法,所以应使用try/finally块,更简单的方法是使用using语句,这等同于try/finally块,将确保在对象离开作用域时自动调用Dispose()方法:- using(var myClass = new MyClass())
- {
- // do your processing
- }
复制代码.NET的一些类如果要关闭资源(如文件或数据库),就有Close()和Dispose()方法,其Close()方法只是调用了Dispose(),新的类只实现了Dispose()方法,因为这个模式已经被大家所熟悉。
IDisposable接口和析构函数的规范
- 如果类定义了实现IDisposable的成员,该类也应该实现IDisposable。
- 实现IDisposable并不意味着也要实现析构函数,析构函数会带来额外的开销。只有包含非托管资源时才应该实现析构函数。
- 如果实现了析构函数,就应该实现IDisposable接口,以便非托管资源可以确定性释放,而不用等到垃圾回收时才释放。
- 析构函数的执行顺序是没有保证的,不能在析构函数中访问其他托管对象,它们的析构函数可能会先执行。
- 要确保Dispose()方法可以重入(可被多次调用)。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |