一.非托管资源

在《垃圾回收算法之引用计数算法》、《垃圾回收算法之引用跟踪算法》和《垃圾回收算法之引用跟踪算法》这3篇文章中,我们介绍了垃圾回收的一些基本概念和原理,但需要说明的是:这些文章中,介绍的都是对托管资源的回收,所谓托管资源,直白一点,你可以理解为托管堆上分配的对象,它由GC来自动管理。

但本节,我们要介绍另外一种资源——非托管资源,它不是分配在托管堆上的资源,而是诸如文件、网络连接、网络套接字Socket、Windows互斥内核对象等其他的资源。

C#程序有时也需要使用非托管资源,如我们使用FileStream打开文件句柄,并使用句柄操作文件,这里的文件就是非托管资源,当我们使用完FileStream时,GC会在某个时间点回收FileStream,但文件不是托管资源,GC对它一无所知,这样会造成内存的泄漏。为了应对这种情况,CLR提供了一种终结(Finalize)机制,以帮助程序释放非托管资源。

二.终结原理

1.Finalize方法

在语法上,C#的Finalize方法非常类似于C++中的析构器,在类名前添加~符号来定义Finalize方法,CLR为Finalize方法生成名为Finalize的protected overvide方法,方法体被try..finally方法块包裹,在finally中调用了base.Finalize方法.

如下所示:

public class TestClass
{
~TestClass()
{
}
}

生所IL代码如下:

Finalize机制允许CLR在判断对象为垃圾之后,但在回收垃圾之前执行一些代码,即执行Finalize方法,比如你可以在这些代码里回收非托管资源,下一步,CLR就可以回收托管堆上的资源(托管资源)了.

注意,并不是说你只能在Finalize方法中回收非托管资源,只是一种习惯性做法。

实际上,非托管资源也是一定要先于托管资源回收的。这是为什么呢?假设一个对象被判断为垃圾, 由于CLR对于非托管资源一无所知,CLR先回收了托管资源,如果在Finalize方法内部需要访问托管资源,则会造成内存泄漏,相反,非托管资源先释放掉,那么剩下的托管资源由于真正的不可达(既没有被非托管资源访问也没有被托管资源访问),就可以被GC垃圾回收了。

那一个对象的托管资源和非托管资源可不可以一起回收呢?答案也是不行的.因为,CLR采用一个特殊的、高优先级的专用线程调用Finalize方法(这样做是为了避免潜在的线程同步问题,使用应用程序的普通优先级线程就有可能发生空上问题),无法保证一起回收,它甚至不保证多个Finalize方法的调用顺序。

在接下来介绍的Finalize内部工作原理时,我们会介绍到freachable队列,特殊线程就是监控该队列的数据,freachable队列为空时,线程将睡眠,但一旦队列中有记录项出现时,线程就会被唤醒,将每一项从freachable队列中移除,同时调用每个对象的Finalize方法。

2.Fianlize的内部工作原理

我们来通过《CLR via C#》中的例子来说明Finalize的内部工作原理,在这之前我们要说明两个概念:

a.终结列表:用来存储实现了Finalize方法的对象指针列表,注意,CLR认为,如果你是从System.Object中继承了Finalize方法,则不会认为你是终结对象,但如果你重写了Finalize方法,则CLR认为对        象是终结对象,则会将它加入终结列表。那在程序运行的时候,对象何时加入终结列表呢?《CLR via C#》中说,在应用程序创建新对象时,该类型的实例构造函数被调用之前。

b.freachable:全称是Finalization Reachable List,它存储着所有被判断为垃圾的终结对象,等待着CLR专用线程对它的调用。

明白了以上的概念,现在我们来图解Finalize的内部工作原理。

    如图所示,在初始状态下,在G0中,A C E F是可达的,C D I实现了Finalize方法,被加入了终结列表(即我们上面所说的概念a),freachable队列为空:

    

    现在,GC开始扫描所有的根,形成对象可达图,注意GC会发现B D G H I J均为垃圾(即同步块索引中的一位标志为0),同时发现D和I虽然为垃圾,但是它是终结对象,因此将它们放入freachable列表中(上面所说的概念b),因为Finalize在被CLR专用线程调用时,这个对象必然要是存活的,所以使得freachable的这些终结对象(D和I)“复活”,同时它们引用的对象(J)也复活了。经过这两个步骤,GC形成了对象可达图,如下所示:

    

    下面开始进行GC回收垃圾工作,在清除垃圾后,B G H被清掉,剩下的对象被压缩并提升至G1代中。随后,特殊的进程清空freachable队列,执行每个对象(这里是D和I)的Finalize方法:

    

   执行完freachable的Finalize方法后,D I J现在没有任何对象引用它们,它们将在下次的GC组成对象可达图时,变得不可达(即垃圾)。在第二次(也可能是某一次)GC时,D I J被清除掉。

这里需要说明

a:终结对象的清除需要两次垃圾回收才能释放它们占用的垃圾;

b:这两次垃圾回收有可能不是连续的,因为GC执行第一次垃圾回收的,终结对象被提升至下一代,而在进行下一代的垃圾回收之前,前一代很有可能进行了1次或多次的垃圾回收。

3.Finalize方法的缺陷

a.因为可终结对象在调用时必须存活,造成可终结对象要经过两次释放才能真正释放掉资源,并在GC中提升至下一代,其引用的对象也会被提升,使对象活得比正常时间长,这增大了内存消耗;

b.Finalize方法的执行时间和执行顺序是控制不了的,因为只有GC完成后才会运行Finalize,而只有应用程序请求更多的内存而不够时才会进行GC;

c.根据b点说明,我们不可以在一个终结对象的Fialize方法中调用另一个终结对象,因为Finalize方法的执行顺序控制不了,我们无法保证调用时另一个终结对象还存在,但可以安全地访问值类型的类型;

    d.CLR用专用线程调用Finalize方法来避免死锁,如果Finalize方法阻塞,则特殊线程也会发生阻塞,无法调用更多的Finalize方法,这使得GC永远回收不了终结对象占用的内存,内存则会一直泄漏;同时如果Finalize未处理的异常则会造成进程终止,无法捕捉该异常。

三.Dispose模式

前面我们介绍了Finalize的缺陷,其中c点,我们可以在程序中控制Finalize不要访问另外一个终结对象,对于d点,我们可以在Finalize中进行异常控制;对于a和b点,我们就不能控制了,这会带来一个致命的问题,如果我的非托管资源很少,在应对高并发的请求时,GC又不知道何时执行,非托管资源又在GC之后,对于非托管资源的释放成为性能的瓶颈,比如Socket等。

微软提供了一个Dispose模式来解决这个问题,它让我们能够显式地释放非托管资源,控制非托管资源的生存期。

实现了IDisposable接口,就实现了dispose模式。

  1. Dispose模式的设计原则
  2. a.可以重复调用Dispose方法

  1. b.析构函数应该Dispose带参方法来释放非托管资源
  2. c.Dispose方法应该可以释放托管资源和非托管资源
  3. d.Dispose方法应该调用GC.SuppressFinalize()方法,指示垃圾回收器不再重复回收该对象

  • e.CLR为继承了IDisposable接口的类提供了特殊的语法糖,使用using(MyDispose myOjb=new MyDispoe()){ … },它会在跳出using的区域时调用MyDispose的Dispose方法。

微软在官方网站上提供了Dispose模式的案例,如下所示

using System;

class BaseClass : IDisposable
{
// 标志位:标志Dispose方法是否被调用过
bool disposed = false; // 实现IDisposable接口
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
} // True时:释放托管和非托管资源,手工调用
//False时:只释放非托管资源,CLR专用线程调用
protected virtual void Dispose(bool disposing)
{
if (disposed)
return; if (disposing) {
// 释放托管资源
} // 释放所有的非托管资源
disposed = true;
} ~BaseClass()
{
Dispose(false);
}
}

2.源码学习:看下.Net Framework中FileStrem的Dispose模式

a.FileStreamr的基类Stream实现了Dispose模式

public abstract class Stream : IDisposable {
public void Dispose()
{
//通过Close方法释放托管资源和非托管资源,同时通知GC
Close();
}
public virtual void Close()
{
Dispose(true);
GC.SuppressFinalize(this);
}
//虚方法,留给FileSteam去实现
protected virtual void Dispose(bool disposing)
{ }
}

b.FileStream类,实现了Finalize方法,并重写了Dispose带参方法,当然FileStream的方法实现了很多功能,写法也较复杂,我们这里只需要了解一下关注的Dispose模式即可。

public class FileStream : Stream{
~FileStream(){
if (_handle != null) {
Dispose(false);//调用基类的Dispose方法,释放非托管资源
}
}
protected override void Dispose(bool disposing)
{
try {
if (_handle != null && !_handle.IsClosed) {
if (_writePos > ) {
FlushWrite(!disposing);//在这里释放资源
}
}
}
finally {
if (_handle != null && !_handle.IsClosed){
_handle.Dispose();
_canRead = false;
_canWrite = false;
_canSeek = false;
base.Dispose(disposing);
}
}
}

参考文档

1.《CLR via C#》(第4版)

2. https://msdn.microsoft.com/en-us/library/system.idisposable(v=vs.110).aspx

3. https://www.zhihu.com/question/46462047Philip Chan的回答

4. http://blog.csdn.net/qing101/article/details/52484987

5. https://www.zhihu.com/question/29265003

6.《.Net最佳实践》

CLR回收非托管资源的更多相关文章

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

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

  2. C# 托管资源与非托管资源

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

  3. 5.C#释放非托管资源1

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

  4. C#编程(七十四)----------释放非托管资源

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

  5. C# 托管非托管资源释放

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

  6. 转/ C# 托管资源和非托管资源

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

  7. C# 托管资源和非托管资源

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

  8. [转]在C#中使用托管资源和非托管资源的区别,以及怎样手动释放非托管资源:

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

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

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

随机推荐

  1. MySQL——优化ORDER BY语句

    本篇文章我们将了解ORDER BY语句的优化,在此之前,你需要对索引有基本的了解,不了解的朋友们可以先看一下我之前写过的索引相关文章.现在让我们开始吧. MySQL中的两种排序方式 1.通过有序索引顺 ...

  2. windows 服务器MYSQL 数据库安装配置

    一.到官网下载MYSQL 打开官网地址:www.mysql.com, 选择 DOWNLOADS,进入到MySQL的下载页面,在页面的底部有一个MySQL Community Edition, 并且下面 ...

  3. 固态硬盘和机械硬盘的比较和SQLSERVER在两种硬盘上的性能差异

    固态硬盘和机械硬盘的比较和SQLSERVER在两种硬盘上的性能差异 在看这篇文章之前可以先看一下下面的文章: SSD小白用户收货!SSD的误区如何解决 这样配会损失性能?实测6种特殊装机方式 听说固态 ...

  4. 调整 Windows VM 的大小

    本文说明如何使用 Azure Powershell 调整在 Resource Manager 部署模型中创建的 Windows VM 的大小. 创建虚拟机 (VM) 后,可以通过更改 VM 大小来扩展 ...

  5. 最近用到的 sql 统计操作

    统计操作 1.分组统计group by select id,name,count(*) as '总数'  from test1 group by id,name     --group by   分组 ...

  6. 解决Hsqldb指针只能单向移动,不能回滚问题(.first())

    Class.forName("org.hsqldb.jdbcDriver").newInstance(); Connection con = java.sql.DriverMana ...

  7. MySQL出现Waiting for table metadata lock的场景浅析

    MySQL版本为5.6.12. 在进行alter table操作时,有时会出现Waiting for table metadata lock的等待场景.而且,一旦alter table TableA的 ...

  8. Linux 文本处理命令

    最近在使用 BASH 进行处理 文本文件的时候,对于文本处理真的是力不从心,今天进行搜集一下linux 中文本处理相关的命令,这样你在进行书写shell 脚本的时候,就能写出更好的方案. 命令搜集: ...

  9. saxbuilder用法

    xml为我们在网络交换数据带来很大方便,在java中可以使用saxbuilder来操作xml格式文件,下面介绍一下saxbuilder的常用方法. import org.jdom.Document; ...

  10. CentOS7.5搭建Flask环境python3.6+mysql+redis+virtualenv

    wget安装环境装备 yum install gcc patch libffi-devel python-devel zlib-devel bzip2-devel openssl-devel ncur ...